@lensagent/core 0.1.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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -0
  3. package/config/lens.example.jsonc +92 -0
  4. package/config/pair.example.json +3 -0
  5. package/dist/adapters/executor.d.ts +35 -0
  6. package/dist/adapters/executor.js +296 -0
  7. package/dist/adapters/lsp.d.ts +18 -0
  8. package/dist/adapters/lsp.js +218 -0
  9. package/dist/adapters/mcp.d.ts +32 -0
  10. package/dist/adapters/mcp.js +283 -0
  11. package/dist/adapters/revert.d.ts +12 -0
  12. package/dist/adapters/revert.js +155 -0
  13. package/dist/config/runtime.d.ts +55 -0
  14. package/dist/config/runtime.js +371 -0
  15. package/dist/config/schema.d.ts +292 -0
  16. package/dist/config/schema.js +177 -0
  17. package/dist/contracts/adapter.d.ts +28 -0
  18. package/dist/contracts/adapter.js +8 -0
  19. package/dist/contracts/capability.d.ts +41 -0
  20. package/dist/contracts/capability.js +1 -0
  21. package/dist/contracts/domain.d.ts +61 -0
  22. package/dist/contracts/domain.js +1 -0
  23. package/dist/contracts/index.d.ts +5 -0
  24. package/dist/contracts/index.js +5 -0
  25. package/dist/contracts/model.d.ts +77 -0
  26. package/dist/contracts/model.js +1 -0
  27. package/dist/contracts/ports.d.ts +14 -0
  28. package/dist/contracts/ports.js +14 -0
  29. package/dist/core/access.d.ts +24 -0
  30. package/dist/core/access.js +205 -0
  31. package/dist/core/codec.d.ts +33 -0
  32. package/dist/core/codec.js +105 -0
  33. package/dist/core/domain.d.ts +13 -0
  34. package/dist/core/domain.js +24 -0
  35. package/dist/core/host.d.ts +19 -0
  36. package/dist/core/host.js +249 -0
  37. package/dist/core/identity.d.ts +19 -0
  38. package/dist/core/identity.js +54 -0
  39. package/dist/entry/bun.d.ts +1 -0
  40. package/dist/entry/bun.js +2 -0
  41. package/dist/entry/node.d.ts +2 -0
  42. package/dist/entry/node.js +3 -0
  43. package/dist/entry/rebuild.d.ts +1 -0
  44. package/dist/entry/rebuild.js +47 -0
  45. package/dist/entry/run.d.ts +1 -0
  46. package/dist/entry/run.js +43 -0
  47. package/dist/execution/lsp-process.d.ts +14 -0
  48. package/dist/execution/lsp-process.js +234 -0
  49. package/dist/http/server.d.ts +34 -0
  50. package/dist/http/server.js +1332 -0
  51. package/dist/launch.d.ts +3 -0
  52. package/dist/launch.js +4 -0
  53. package/dist/runtime/agent.d.ts +35 -0
  54. package/dist/runtime/agent.js +853 -0
  55. package/dist/runtime/artifacts.d.ts +15 -0
  56. package/dist/runtime/artifacts.js +72 -0
  57. package/dist/runtime/cache.d.ts +20 -0
  58. package/dist/runtime/cache.js +44 -0
  59. package/dist/runtime/calls.d.ts +21 -0
  60. package/dist/runtime/calls.js +130 -0
  61. package/dist/runtime/capabilities.d.ts +58 -0
  62. package/dist/runtime/capabilities.js +196 -0
  63. package/dist/runtime/cleanup.d.ts +7 -0
  64. package/dist/runtime/cleanup.js +33 -0
  65. package/dist/runtime/context.d.ts +34 -0
  66. package/dist/runtime/context.js +169 -0
  67. package/dist/runtime/diagnostics.d.ts +8 -0
  68. package/dist/runtime/diagnostics.js +71 -0
  69. package/dist/runtime/events.d.ts +22 -0
  70. package/dist/runtime/events.js +131 -0
  71. package/dist/runtime/kernel-capabilities.d.ts +32 -0
  72. package/dist/runtime/kernel-capabilities.js +200 -0
  73. package/dist/runtime/manifests.d.ts +14 -0
  74. package/dist/runtime/manifests.js +117 -0
  75. package/dist/runtime/model.d.ts +20 -0
  76. package/dist/runtime/model.js +241 -0
  77. package/dist/runtime/observability.d.ts +22 -0
  78. package/dist/runtime/observability.js +104 -0
  79. package/dist/runtime/policy.d.ts +8 -0
  80. package/dist/runtime/policy.js +31 -0
  81. package/dist/runtime/profile.d.ts +8 -0
  82. package/dist/runtime/profile.js +52 -0
  83. package/dist/runtime/projects.d.ts +40 -0
  84. package/dist/runtime/projects.js +115 -0
  85. package/dist/runtime/results.d.ts +7 -0
  86. package/dist/runtime/results.js +55 -0
  87. package/dist/runtime/review.d.ts +9 -0
  88. package/dist/runtime/review.js +54 -0
  89. package/dist/runtime/scheduler.d.ts +15 -0
  90. package/dist/runtime/scheduler.js +85 -0
  91. package/dist/runtime/skills.d.ts +2 -0
  92. package/dist/runtime/skills.js +83 -0
  93. package/dist/runtime/tokenizer.d.ts +8 -0
  94. package/dist/runtime/tokenizer.js +30 -0
  95. package/dist/store/database.d.ts +3 -0
  96. package/dist/store/database.js +49 -0
  97. package/dist/store/event-log.d.ts +19 -0
  98. package/dist/store/event-log.js +61 -0
  99. package/dist/store/schema.d.ts +1 -0
  100. package/dist/store/schema.js +347 -0
  101. package/dist/store/store.d.ts +344 -0
  102. package/dist/store/store.js +2554 -0
  103. package/package.json +73 -0
  104. package/proto/executor.proto +314 -0
@@ -0,0 +1,249 @@
1
+ import { mkdir, readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { id } from './codec.js';
4
+ import { PairStore } from './access.js';
5
+ import { ConfigRuntime } from '../config/runtime.js';
6
+ import { openDatabase } from '../store/database.js';
7
+ import { Store } from '../store/store.js';
8
+ import { EventLog } from '../store/event-log.js';
9
+ import { EventBus } from '../runtime/events.js';
10
+ import { Scheduler } from '../runtime/scheduler.js';
11
+ import { ArtifactStore } from '../runtime/artifacts.js';
12
+ import { AiModelGateway } from '../runtime/model.js';
13
+ import { AutoReviewer } from '../runtime/review.js';
14
+ import { ProjectRuntimes } from '../runtime/projects.js';
15
+ import { Observability } from '../runtime/observability.js';
16
+ import { adapterTarget, assertManifestSet, coreManifests } from '../runtime/manifests.js';
17
+ import { ExecutorClient, executorCapabilities } from '../adapters/executor.js';
18
+ import { mcpCapabilities } from '../adapters/mcp.js';
19
+ import { LspPool, lspCapabilities } from '../adapters/lsp.js';
20
+ import { revertCapabilities } from '../adapters/revert.js';
21
+ import { applyPurge } from '../runtime/cleanup.js';
22
+ import { createServer } from '../http/server.js';
23
+ export async function launchCore(options = {}) {
24
+ const root = resolve(options.installRoot ?? process.cwd());
25
+ const config = new ConfigRuntime(root, options.configPath);
26
+ await config.load();
27
+ const target = adapterTarget(options.runtime ?? 'node');
28
+ assertManifestSet(coreManifests(config.current.config), target);
29
+ const blockers = new Set();
30
+ const observability = new Observability(root, config.current.config.observability);
31
+ const events = new EventBus(id('instance'), config.current.config.sse, () => config.current.metadataRevision + pair.metadataRevision);
32
+ const pair = new PairStore(root, config.current.config.pairPath ?? 'config/pair.json', (clientId) => events.revoke(clientId));
33
+ try {
34
+ await pair.load();
35
+ }
36
+ catch (error) {
37
+ blockers.add('pair');
38
+ observability.record('access', 'pair_load', 'failed', 0, { error_code: errorCode(error) });
39
+ }
40
+ const eventLogConfig = config.current.config.persistence?.eventLog;
41
+ const eventLog = eventLogConfig?.enabled === true ? new EventLog(root, eventLogConfig.path, eventLogConfig.fsync) : undefined;
42
+ await eventLog?.load();
43
+ const store = new Store(openDatabase(root, config.current.config.database.path), events, eventLog);
44
+ const interrupted = store.recover();
45
+ if (interrupted.length > 0)
46
+ observability.record('store', 'recovery', 'success', 0, { interrupted: interrupted.length });
47
+ // Not `root`. The app root is where config/lens.jsonc holds the Provider API keys and where
48
+ // config/pair.json holds the client allowlist, so making it the default Workspace put both inside
49
+ // the one directory an Agent's file Capabilities are allowed to reach: `read_file` would reach the
50
+ // credentials, and `write_file` on the pair file would let an Agent mark its own client verified.
51
+ // A dedicated subdirectory keeps a working default without making the containment boundary and the
52
+ // secret store the same path.
53
+ const defaultWorkspace = resolve(root, 'workspace');
54
+ await mkdir(defaultWorkspace, { recursive: true });
55
+ const defaultProject = store.defaultProject() ?? store.createProject('Project', [defaultWorkspace]);
56
+ const defaultProjectId = string(defaultProject.id);
57
+ const executor = await ExecutorClient.create(root, config);
58
+ try {
59
+ await executor.health();
60
+ }
61
+ catch (error) {
62
+ blockers.add('executor');
63
+ observability.record('executor', 'health', 'failed', 0, { error_code: errorCode(error) });
64
+ }
65
+ const scheduler = new Scheduler(config.current.config.concurrency);
66
+ const artifacts = new ArtifactStore(root, config.current.config.artifacts.directory, store, config.current.config.persistence?.contentStore?.mode ?? 'filesystem');
67
+ const models = new AiModelGateway(config, target);
68
+ const reviewer = new AutoReviewer(models, config);
69
+ const lsp = new LspPool(config.current.config.lspServers ?? {});
70
+ const sharedCapabilities = [
71
+ ...executorCapabilities(executor),
72
+ ...lspCapabilities(config.current.config.lspServers ?? {}, lsp),
73
+ ...revertCapabilities(store, executor)
74
+ ];
75
+ // An MCP server that cannot be reached loses its own tools and nothing else. It used to add a
76
+ // startup blocker, which put the whole runtime into health_only: one unreachable optional
77
+ // integration returned 503 from readiness and no client could sync. 02A also fixes the readiness
78
+ // blocker codes at PAIR_FILE_INVALID, MIGRATION_FAILED, RECOVERY_INCOMPLETE and STORE_UNAVAILABLE,
79
+ // so `mcp:<id>` was never a documented reason to be unready.
80
+ const degradedMcpServers = [];
81
+ for (const [serverId, serverConfig] of Object.entries(config.current.config.mcpServers ?? {})) {
82
+ try {
83
+ sharedCapabilities.push(...await mcpCapabilities(serverId, serverConfig));
84
+ }
85
+ catch (error) {
86
+ degradedMcpServers.push(serverId);
87
+ observability.record('mcp', 'discover', 'failed', 0, { server_id: serverId, error_code: errorCode(error) });
88
+ }
89
+ }
90
+ const runtimes = new ProjectRuntimes(store, config, scheduler, artifacts, models, reviewer, sharedCapabilities, target, events, defaultProjectId);
91
+ runtimes.get(defaultProjectId);
92
+ runtimes.startIdleEviction();
93
+ // Graceful stop arrives as a request on the Server's control path, not as an OS signal, so the
94
+ // runtime needs a way to hand that request outwards. The Promise is created before the Server so
95
+ // the Route can never fire into an unset resolver.
96
+ let signalShutdown = () => undefined;
97
+ const shutdownRequested = new Promise((resolve) => { signalShutdown = resolve; });
98
+ const https = await httpsOptions(root, config);
99
+ const server = createServer({
100
+ ...(https === undefined ? {} : { https }),
101
+ config,
102
+ pair,
103
+ store,
104
+ events,
105
+ runtime: (projectId) => runtimes.get(projectId),
106
+ refreshProject: (projectId) => runtimes.refresh(projectId),
107
+ models,
108
+ artifacts,
109
+ observability,
110
+ executor,
111
+ defaultProjectId,
112
+ ready: () => ({ mode: blockers.size === 0 ? 'full' : 'health_only', blockers: [...blockers] }),
113
+ requestShutdown: () => { signalShutdown(); }
114
+ });
115
+ await server.listen({ ...config.current.config.listen });
116
+ for (const serverId of degradedMcpServers) {
117
+ events.emit(defaultProjectId, store.stateRevision(), 'runtime.notice', {
118
+ code: 'mcp_server_unavailable',
119
+ server_id: serverId
120
+ });
121
+ }
122
+ pair.start((error) => {
123
+ observability.record('access', 'pair_reload', 'failed', 0, { error_code: errorCode(error) });
124
+ events.emit(defaultProjectId, store.stateRevision(), 'config.changed', { component: 'pair', error: error.message });
125
+ });
126
+ config.start((snapshot) => {
127
+ scheduler.updateLimits(snapshot.config.concurrency);
128
+ events.updateLimits(snapshot.config.sse);
129
+ events.emit(defaultProjectId, store.stateRevision(), 'config.changed', {
130
+ config_digest: snapshot.digest,
131
+ file_digest: snapshot.fileDigest,
132
+ pending_restart: snapshot.pendingRestart,
133
+ error: snapshot.error
134
+ });
135
+ });
136
+ const maintenanceTimer = setInterval(() => {
137
+ void runRetention(store, artifacts, executor, config).catch((error) => {
138
+ observability.record('maintenance', 'retention', 'failed', 0, { error_code: errorCode(error) });
139
+ });
140
+ }, 60 * 60 * 1000);
141
+ maintenanceTimer.unref();
142
+ let closing;
143
+ return {
144
+ url: `${config.current.config.tls.enabled ? 'https' : 'http'}://${config.current.config.listen.host}:${config.current.config.listen.port}`,
145
+ shutdownRequested,
146
+ close: () => {
147
+ if (closing !== undefined)
148
+ return closing;
149
+ closing = (async () => {
150
+ const graceMs = config.current.config.shutdown?.graceMs ?? 30_000;
151
+ const ordered = (async () => {
152
+ const serverClosing = server.close();
153
+ clearInterval(maintenanceTimer);
154
+ config.close();
155
+ pair.close();
156
+ await runtimes.close();
157
+ scheduler.close();
158
+ events.close();
159
+ await lsp.close();
160
+ executor.close();
161
+ await serverClosing;
162
+ await observability.close();
163
+ await store.close();
164
+ })();
165
+ // Bounded, because an unbounded wait here is worse than an imperfect stop. A stage that
166
+ // never settles -- a Worker ignoring cancellation, an LSP child not exiting -- would strand
167
+ // the process with no way out but a hard kill, and a hard kill discards this whole ordered
168
+ // sequence. Section 10 already leaves unconfirmable objects to Recovery and SQLite/WAL is
169
+ // crash-safe, so returning after the grace period loses no committed state.
170
+ await withinGrace(ordered, graceMs, observability);
171
+ })();
172
+ return closing;
173
+ }
174
+ };
175
+ }
176
+ /**
177
+ * Waits for the ordered shutdown, but not forever.
178
+ *
179
+ * The diagnostic is recorded the moment the grace period expires rather than afterwards, because
180
+ * `observability.close()` is itself one of the last stages: recording later could be writing to a
181
+ * sink that the sequence has already closed.
182
+ */
183
+ async function withinGrace(ordered, graceMs, observability) {
184
+ let timer;
185
+ const expired = new Promise((resolve) => {
186
+ timer = setTimeout(() => {
187
+ try {
188
+ observability.record('runtime', 'shutdown', 'failed', graceMs, { reason: 'grace_expired', grace_ms: graceMs });
189
+ }
190
+ catch {
191
+ // A sink that is already gone must not turn a slow stop into a crash.
192
+ }
193
+ resolve('expired');
194
+ }, graceMs);
195
+ // Never the reason the process stays alive.
196
+ timer.unref();
197
+ });
198
+ // Settled either way, so a stage that rejects after the grace period cannot surface as an
199
+ // unhandled rejection once nothing is awaiting it any more.
200
+ const settled = ordered.then(() => 'closed', (error) => {
201
+ try {
202
+ observability.record('runtime', 'shutdown', 'failed', 0, { error_code: errorCode(error) });
203
+ }
204
+ catch {
205
+ // As above.
206
+ }
207
+ return 'failed';
208
+ });
209
+ await Promise.race([settled, expired]);
210
+ if (timer !== undefined)
211
+ clearTimeout(timer);
212
+ }
213
+ async function runRetention(store, artifacts, executor, config) {
214
+ const retention = config.current.config.historyRetention;
215
+ if (retention.purgeMode !== 'after_days' || retention.purgeArchivedAfterDays === null)
216
+ return;
217
+ const before = new Date(Date.now() - retention.purgeArchivedAfterDays * 24 * 60 * 60 * 1000).toISOString();
218
+ const projectIds = [];
219
+ let cursor;
220
+ do {
221
+ const page = store.listProjects({ ...(cursor === undefined ? {} : { cursor }), limit: 500 }, 'all');
222
+ for (const project of page.items) {
223
+ if (typeof project.id === 'string')
224
+ projectIds.push(project.id);
225
+ }
226
+ cursor = page.nextCursor ?? undefined;
227
+ } while (cursor !== undefined);
228
+ for (const projectId of projectIds) {
229
+ await applyPurge(store.purgeArchived(projectId, before), artifacts, executor);
230
+ }
231
+ }
232
+ async function httpsOptions(root, config) {
233
+ const tls = config.current.config.tls;
234
+ if (!tls.enabled)
235
+ return undefined;
236
+ if (tls.keyFile === null || tls.certFile === null)
237
+ throw new Error('TLS keyFile and certFile are required when TLS is enabled.');
238
+ return { key: await readFile(resolve(root, tls.keyFile)), cert: await readFile(resolve(root, tls.certFile)) };
239
+ }
240
+ function string(value) {
241
+ if (typeof value !== 'string')
242
+ throw new Error('Expected a string value.');
243
+ return value;
244
+ }
245
+ function errorCode(error) {
246
+ if (typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string')
247
+ return error.code;
248
+ return error instanceof Error ? error.name : 'UNKNOWN_ERROR';
249
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Component identity as the Launcher design defines it: component kind, independent software
3
+ * version and the integer protocol versions this build speaks.
4
+ *
5
+ * `package.json` is the single source. The Launcher rereads installed metadata offline after an
6
+ * Update and before a Start, so the values cannot live only in a running Server's response, and
7
+ * a second hand-maintained copy in source would be free to disagree with the published version.
8
+ *
9
+ * The two protocol versions are independent integers compared by strict equality against the
10
+ * adjacent component; they are not SemVer and carry no range or negotiation semantics.
11
+ * `runtime_contract_digest` is unrelated and is not a protocol version.
12
+ */
13
+ export interface ComponentIdentity {
14
+ readonly component: string;
15
+ readonly softwareVersion: string;
16
+ readonly protocols: Readonly<Record<string, number>>;
17
+ }
18
+ /** Reads the installed package metadata once per process. */
19
+ export declare function componentIdentity(): ComponentIdentity;
@@ -0,0 +1,54 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ // Anchored to this module's own URL rather than `cwd`, for the same reason as the bundled
5
+ // `executor.proto` path: the process `cwd` is the component's app root, which is not the
6
+ // directory the package was installed into.
7
+ const manifestPath = resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json');
8
+ let cached;
9
+ /** Reads the installed package metadata once per process. */
10
+ export function componentIdentity() {
11
+ cached ??= readManifest();
12
+ return cached;
13
+ }
14
+ function readManifest() {
15
+ let document;
16
+ try {
17
+ document = JSON.parse(readFileSync(manifestPath, 'utf8'));
18
+ }
19
+ catch (error) {
20
+ throw new Error(`Package metadata at ${manifestPath} could not be read: ${message(error)}`);
21
+ }
22
+ const manifest = object(document, 'package.json');
23
+ const lens = object(manifest.lens, 'package.json "lens"');
24
+ return {
25
+ component: string(lens.component, 'package.json "lens.component"'),
26
+ softwareVersion: string(manifest.version, 'package.json "version"'),
27
+ protocols: protocols(lens.protocols)
28
+ };
29
+ }
30
+ function protocols(value) {
31
+ const entries = object(value, 'package.json "lens.protocols"');
32
+ const result = {};
33
+ for (const [name, version] of Object.entries(entries)) {
34
+ if (typeof version !== 'number' || !Number.isInteger(version)) {
35
+ throw new Error(`Protocol version "${name}" in ${manifestPath} must be an integer.`);
36
+ }
37
+ result[name] = version;
38
+ }
39
+ return result;
40
+ }
41
+ function object(value, field) {
42
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
43
+ throw new Error(`${field} in ${manifestPath} must be an object.`);
44
+ }
45
+ return value;
46
+ }
47
+ function string(value, field) {
48
+ if (typeof value !== 'string' || value === '')
49
+ throw new Error(`${field} in ${manifestPath} must be a non-empty string.`);
50
+ return value;
51
+ }
52
+ function message(error) {
53
+ return error instanceof Error ? error.message : String(error);
54
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { run } from './run.js';
2
+ await run('bun');
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { run } from './run.js';
3
+ await run('node');
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ import { dirname, resolve } from 'node:path';
2
+ import { mkdir } from 'node:fs/promises';
3
+ import { openDatabase } from '../store/database.js';
4
+ import { readEventLog } from '../store/event-log.js';
5
+ const eventLog = argument('--event-log');
6
+ const output = argument('--output');
7
+ await mkdir(dirname(resolve(output)), { recursive: true });
8
+ const records = await readEventLog(resolve(eventLog));
9
+ const state = records.at(-1)?.state;
10
+ if (typeof state !== 'object' || state === null || Array.isArray(state))
11
+ throw new Error('Event Log has no rebuildable state.');
12
+ const stateRecord = state;
13
+ const database = openDatabase(process.cwd(), output);
14
+ const tables = Object.keys(stateRecord);
15
+ database.pragma('foreign_keys = OFF');
16
+ const restore = database.transaction(() => {
17
+ for (const table of [...tables].reverse())
18
+ database.prepare(`DELETE FROM ${table}`).run();
19
+ for (const table of tables) {
20
+ const rows = stateRecord[table];
21
+ if (!Array.isArray(rows))
22
+ throw new Error(`Event Log table ${table} is invalid.`);
23
+ for (const row of rows) {
24
+ if (typeof row !== 'object' || row === null || Array.isArray(row))
25
+ throw new Error(`Event Log row ${table} is invalid.`);
26
+ const rowRecord = row;
27
+ const columns = Object.keys(rowRecord);
28
+ database.prepare(`INSERT INTO ${table}(${columns.join(', ')}) VALUES (${columns.map(() => '?').join(', ')})`).run(...columns.map((column) => scalar(rowRecord[column])));
29
+ }
30
+ }
31
+ });
32
+ restore();
33
+ database.pragma('foreign_keys = ON');
34
+ database.close();
35
+ process.stdout.write(`rebuilt ${output} at revision ${records.at(-1)?.revision ?? 0}\n`);
36
+ function argument(name) {
37
+ const index = process.argv.indexOf(name);
38
+ const value = process.argv[index + 1];
39
+ if (index < 0 || value === undefined)
40
+ throw new Error(`${name} is required.`);
41
+ return value;
42
+ }
43
+ function scalar(value) {
44
+ if (value === null || typeof value === 'string' || typeof value === 'number')
45
+ return value;
46
+ throw new Error('Event Log contains a non-scalar database value.');
47
+ }
@@ -0,0 +1 @@
1
+ export declare function run(runtime: 'node' | 'bun'): Promise<void>;
@@ -0,0 +1,43 @@
1
+ import { launch } from '../launch.js';
2
+ export async function run(runtime) {
3
+ ignoreStdoutErrors();
4
+ const server = await launch({ runtime });
5
+ announce(`lens-core listening at ${server.url}\n`);
6
+ listenForShutdown(server);
7
+ }
8
+ function listenForShutdown(server) {
9
+ let closing = false;
10
+ const close = () => {
11
+ if (closing)
12
+ return;
13
+ closing = true;
14
+ void server.close().finally(() => process.exit(0));
15
+ };
16
+ // The graceful stop a process manager is expected to use: a request on the Server's control
17
+ // path, not a signal. The signal handlers stay for an interactive terminal and for anything that
18
+ // does send one, but they are not the documented Stop mechanism.
19
+ void server.shutdownRequested.then(close);
20
+ process.once('SIGINT', close);
21
+ process.once('SIGTERM', close);
22
+ }
23
+ /**
24
+ * A detached Core keeps running after the program that started it exits, and it must not depend on
25
+ * that program's stdio pipes. When the read end of an inherited stdout pipe closes, the next write
26
+ * raises EPIPE — asynchronously, as a stream `error` event, so `try`/`catch` around the write does
27
+ * not see it and the default handling ends the process. Swallowing it here keeps a healthy runtime
28
+ * alive; stdout is a startup courtesy, not a runtime dependency. Diagnostics go to the
29
+ * Observability log sink, which is unaffected.
30
+ */
31
+ function ignoreStdoutErrors() {
32
+ process.stdout.on('error', () => undefined);
33
+ }
34
+ function announce(line) {
35
+ // Also guarded synchronously: an already-closed or non-writable stdout throws from `write`
36
+ // itself rather than emitting.
37
+ try {
38
+ process.stdout.write(line);
39
+ }
40
+ catch {
41
+ // Nothing to report to, and nothing about the runtime is wrong.
42
+ }
43
+ }
@@ -0,0 +1,14 @@
1
+ export declare class LspProcess {
2
+ #private;
3
+ readonly maxFrameBytes: number;
4
+ readonly maxBufferBytes: number;
5
+ constructor(command: string, args: readonly string[], cwd: string, maxFrameBytes: number, maxBufferBytes: number);
6
+ get pid(): number | undefined;
7
+ get initialized(): boolean;
8
+ get closed(): boolean;
9
+ markInitialized(): void;
10
+ request(method: string, params: unknown, signal?: AbortSignal, allowClosing?: boolean): Promise<unknown>;
11
+ notify(method: string, params: unknown, allowClosing?: boolean): void;
12
+ drainNotifications(): readonly unknown[];
13
+ close(): Promise<void>;
14
+ }