twinclaw 1.0.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 (132) hide show
  1. package/README.md +66 -0
  2. package/bin/npm-twinclaw.js +17 -0
  3. package/bin/run-twinbot-cli.js +36 -0
  4. package/bin/twinbot.js +4 -0
  5. package/bin/twinclaw.js +4 -0
  6. package/dist/api/handlers/browser.js +160 -0
  7. package/dist/api/handlers/callback.js +80 -0
  8. package/dist/api/handlers/config-validate.js +19 -0
  9. package/dist/api/handlers/health.js +117 -0
  10. package/dist/api/handlers/local-state-backup.js +118 -0
  11. package/dist/api/handlers/persona-state.js +59 -0
  12. package/dist/api/handlers/skill-packages.js +94 -0
  13. package/dist/api/router.js +278 -0
  14. package/dist/api/runtime-event-producer.js +99 -0
  15. package/dist/api/shared.js +82 -0
  16. package/dist/api/websocket-hub.js +305 -0
  17. package/dist/config/config-loader.js +2 -0
  18. package/dist/config/env-schema.js +202 -0
  19. package/dist/config/env-validator.js +223 -0
  20. package/dist/config/identity-bootstrap.js +115 -0
  21. package/dist/config/json-config.js +344 -0
  22. package/dist/config/workspace.js +186 -0
  23. package/dist/core/channels-cli.js +77 -0
  24. package/dist/core/cli.js +119 -0
  25. package/dist/core/context-assembly.js +33 -0
  26. package/dist/core/doctor.js +365 -0
  27. package/dist/core/gateway-cli.js +323 -0
  28. package/dist/core/gateway.js +416 -0
  29. package/dist/core/heartbeat.js +54 -0
  30. package/dist/core/install-cli.js +320 -0
  31. package/dist/core/lane-executor.js +134 -0
  32. package/dist/core/logs-cli.js +70 -0
  33. package/dist/core/onboarding.js +760 -0
  34. package/dist/core/pairing-cli.js +78 -0
  35. package/dist/core/secret-vault-cli.js +204 -0
  36. package/dist/core/types.js +1 -0
  37. package/dist/index.js +404 -0
  38. package/dist/interfaces/dispatcher.js +214 -0
  39. package/dist/interfaces/telegram_handler.js +82 -0
  40. package/dist/interfaces/tui-dashboard.js +53 -0
  41. package/dist/interfaces/whatsapp_handler.js +94 -0
  42. package/dist/release/cli.js +97 -0
  43. package/dist/release/mvp-gate-cli.js +118 -0
  44. package/dist/release/twinbot-config-schema.js +162 -0
  45. package/dist/release/twinclaw-config-schema.js +162 -0
  46. package/dist/services/block-chunker.js +174 -0
  47. package/dist/services/browser-service.js +334 -0
  48. package/dist/services/context-lifecycle.js +314 -0
  49. package/dist/services/db.js +1055 -0
  50. package/dist/services/delivery-tracker.js +110 -0
  51. package/dist/services/dm-pairing.js +245 -0
  52. package/dist/services/embedding-service.js +125 -0
  53. package/dist/services/file-watcher.js +125 -0
  54. package/dist/services/inbound-debounce.js +92 -0
  55. package/dist/services/incident-manager.js +516 -0
  56. package/dist/services/job-scheduler.js +176 -0
  57. package/dist/services/local-state-backup.js +682 -0
  58. package/dist/services/mcp-client-adapter.js +291 -0
  59. package/dist/services/mcp-server-manager.js +143 -0
  60. package/dist/services/model-router.js +927 -0
  61. package/dist/services/mvp-gate.js +845 -0
  62. package/dist/services/orchestration-service.js +422 -0
  63. package/dist/services/persona-state.js +256 -0
  64. package/dist/services/policy-engine.js +92 -0
  65. package/dist/services/proactive-notifier.js +94 -0
  66. package/dist/services/queue-service.js +146 -0
  67. package/dist/services/release-pipeline.js +652 -0
  68. package/dist/services/runtime-budget-governor.js +415 -0
  69. package/dist/services/secret-vault.js +704 -0
  70. package/dist/services/semantic-memory.js +249 -0
  71. package/dist/services/skill-package-manager.js +806 -0
  72. package/dist/services/skill-registry.js +122 -0
  73. package/dist/services/streaming-output.js +75 -0
  74. package/dist/services/stt-service.js +39 -0
  75. package/dist/services/tts-service.js +44 -0
  76. package/dist/skills/builtin.js +250 -0
  77. package/dist/skills/shell.js +87 -0
  78. package/dist/skills/types.js +1 -0
  79. package/dist/types/api.js +1 -0
  80. package/dist/types/context-budget.js +1 -0
  81. package/dist/types/doctor.js +1 -0
  82. package/dist/types/file-watcher.js +1 -0
  83. package/dist/types/incident.js +1 -0
  84. package/dist/types/local-state-backup.js +1 -0
  85. package/dist/types/mcp.js +1 -0
  86. package/dist/types/messaging.js +1 -0
  87. package/dist/types/model-routing.js +1 -0
  88. package/dist/types/mvp-gate.js +2 -0
  89. package/dist/types/orchestration.js +1 -0
  90. package/dist/types/persona-state.js +22 -0
  91. package/dist/types/policy.js +1 -0
  92. package/dist/types/reasoning-graph.js +1 -0
  93. package/dist/types/release.js +1 -0
  94. package/dist/types/reliability.js +1 -0
  95. package/dist/types/runtime-budget.js +1 -0
  96. package/dist/types/scheduler.js +1 -0
  97. package/dist/types/secret-vault.js +1 -0
  98. package/dist/types/skill-packages.js +1 -0
  99. package/dist/types/websocket.js +14 -0
  100. package/dist/utils/logger.js +57 -0
  101. package/dist/utils/retry.js +61 -0
  102. package/dist/utils/secret-scan.js +208 -0
  103. package/mcp-servers.json +179 -0
  104. package/package.json +81 -0
  105. package/skill-packages.json +92 -0
  106. package/skill-packages.lock.json +5 -0
  107. package/src/skills/builtin.ts +275 -0
  108. package/src/skills/shell.ts +118 -0
  109. package/src/skills/types.ts +30 -0
  110. package/src/types/api.ts +252 -0
  111. package/src/types/blessed-contrib.d.ts +4 -0
  112. package/src/types/context-budget.ts +76 -0
  113. package/src/types/doctor.ts +29 -0
  114. package/src/types/file-watcher.ts +26 -0
  115. package/src/types/incident.ts +57 -0
  116. package/src/types/local-state-backup.ts +121 -0
  117. package/src/types/mcp.ts +106 -0
  118. package/src/types/messaging.ts +35 -0
  119. package/src/types/model-routing.ts +61 -0
  120. package/src/types/mvp-gate.ts +99 -0
  121. package/src/types/orchestration.ts +65 -0
  122. package/src/types/persona-state.ts +61 -0
  123. package/src/types/policy.ts +27 -0
  124. package/src/types/reasoning-graph.ts +58 -0
  125. package/src/types/release.ts +115 -0
  126. package/src/types/reliability.ts +43 -0
  127. package/src/types/runtime-budget.ts +85 -0
  128. package/src/types/scheduler.ts +47 -0
  129. package/src/types/secret-vault.ts +62 -0
  130. package/src/types/skill-packages.ts +81 -0
  131. package/src/types/sqlite-vec.d.ts +5 -0
  132. package/src/types/websocket.ts +122 -0
@@ -0,0 +1,806 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { access, readFile, rename, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ const DEFAULT_CATALOG_PATH = path.resolve('skill-packages.json');
6
+ const DEFAULT_LOCK_PATH = path.resolve('skill-packages.lock.json');
7
+ const LOCKFILE_VERSION = 1;
8
+ const DEFAULT_RUNTIME_API_VERSION = '1.0.0';
9
+ const SEMVER_REGEX = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
10
+ const CAPABILITY_SCOPES = new Set([
11
+ 'read-only',
12
+ 'write-limited',
13
+ 'high-risk',
14
+ 'unclassified',
15
+ ]);
16
+ function isRecord(value) {
17
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
18
+ }
19
+ function asNonEmptyString(value, fieldName) {
20
+ if (typeof value !== 'string' || value.trim().length === 0) {
21
+ throw new Error(`Invalid manifest field '${fieldName}'. Expected non-empty string.`);
22
+ }
23
+ return value.trim();
24
+ }
25
+ function parseSemver(version) {
26
+ const normalized = version.trim().replace(/^v/, '');
27
+ const match = SEMVER_REGEX.exec(normalized);
28
+ if (!match) {
29
+ return null;
30
+ }
31
+ return {
32
+ major: Number(match[1]),
33
+ minor: Number(match[2]),
34
+ patch: Number(match[3]),
35
+ prerelease: match[4] ?? null,
36
+ };
37
+ }
38
+ function compareSemver(left, right) {
39
+ const a = parseSemver(left);
40
+ const b = parseSemver(right);
41
+ if (!a || !b) {
42
+ throw new Error(`Cannot compare invalid semver values '${left}' and '${right}'.`);
43
+ }
44
+ if (a.major !== b.major)
45
+ return a.major - b.major;
46
+ if (a.minor !== b.minor)
47
+ return a.minor - b.minor;
48
+ if (a.patch !== b.patch)
49
+ return a.patch - b.patch;
50
+ if (a.prerelease === b.prerelease)
51
+ return 0;
52
+ if (!a.prerelease)
53
+ return 1;
54
+ if (!b.prerelease)
55
+ return -1;
56
+ return a.prerelease.localeCompare(b.prerelease);
57
+ }
58
+ function sortedRecord(value) {
59
+ return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)));
60
+ }
61
+ function normalizeRange(range) {
62
+ return range.trim();
63
+ }
64
+ function parseRangeBase(rangeValue) {
65
+ const parsed = parseSemver(rangeValue);
66
+ if (!parsed) {
67
+ throw new Error(`Invalid semver range token '${rangeValue}'.`);
68
+ }
69
+ return parsed;
70
+ }
71
+ function compareParsedVersion(a, b) {
72
+ if (a.major !== b.major)
73
+ return a.major - b.major;
74
+ if (a.minor !== b.minor)
75
+ return a.minor - b.minor;
76
+ if (a.patch !== b.patch)
77
+ return a.patch - b.patch;
78
+ if (a.prerelease === b.prerelease)
79
+ return 0;
80
+ if (!a.prerelease)
81
+ return 1;
82
+ if (!b.prerelease)
83
+ return -1;
84
+ return a.prerelease.localeCompare(b.prerelease);
85
+ }
86
+ function satisfiesToken(version, token) {
87
+ if (token === '*' || token.length === 0) {
88
+ return true;
89
+ }
90
+ const operators = ['>=', '<=', '>', '<', '='];
91
+ for (const operator of operators) {
92
+ if (token.startsWith(operator)) {
93
+ const parsed = parseRangeBase(token.slice(operator.length));
94
+ const comparison = compareParsedVersion(version, parsed);
95
+ switch (operator) {
96
+ case '>=':
97
+ return comparison >= 0;
98
+ case '<=':
99
+ return comparison <= 0;
100
+ case '>':
101
+ return comparison > 0;
102
+ case '<':
103
+ return comparison < 0;
104
+ case '=':
105
+ return comparison === 0;
106
+ default:
107
+ return false;
108
+ }
109
+ }
110
+ }
111
+ if (token.startsWith('^')) {
112
+ const base = parseRangeBase(token.slice(1));
113
+ if (compareParsedVersion(version, base) < 0)
114
+ return false;
115
+ if (base.major > 0) {
116
+ return version.major === base.major;
117
+ }
118
+ if (base.minor > 0) {
119
+ return version.major === 0 && version.minor === base.minor;
120
+ }
121
+ return version.major === 0 && version.minor === 0 && version.patch === base.patch;
122
+ }
123
+ if (token.startsWith('~')) {
124
+ const base = parseRangeBase(token.slice(1));
125
+ if (compareParsedVersion(version, base) < 0)
126
+ return false;
127
+ return version.major === base.major && version.minor === base.minor;
128
+ }
129
+ const exact = parseRangeBase(token);
130
+ return compareParsedVersion(version, exact) === 0;
131
+ }
132
+ function satisfiesRange(version, range) {
133
+ const parsedVersion = parseSemver(version);
134
+ if (!parsedVersion) {
135
+ return false;
136
+ }
137
+ const normalized = normalizeRange(range);
138
+ if (normalized.length === 0 || normalized === '*') {
139
+ return true;
140
+ }
141
+ const tokens = normalized.split(/\s+/).filter(Boolean);
142
+ return tokens.every((token) => satisfiesToken(parsedVersion, token));
143
+ }
144
+ function cloneConstraintMap(source) {
145
+ const target = new Map();
146
+ for (const [name, ranges] of source) {
147
+ target.set(name, [...ranges]);
148
+ }
149
+ return target;
150
+ }
151
+ function addConstraint(constraints, packageName, range) {
152
+ const normalizedRange = normalizeRange(range);
153
+ const current = constraints.get(packageName) ?? [];
154
+ if (!current.includes(normalizedRange)) {
155
+ current.push(normalizedRange);
156
+ constraints.set(packageName, current);
157
+ }
158
+ }
159
+ function expandConstraintsWithDependencies(constraints, selected) {
160
+ const expanded = cloneConstraintMap(constraints);
161
+ const selectedEntries = [...selected.entries()].sort(([a], [b]) => a.localeCompare(b));
162
+ for (const [, manifest] of selectedEntries) {
163
+ const dependencies = manifest.dependencies ?? {};
164
+ for (const dependencyName of Object.keys(dependencies).sort()) {
165
+ const dependencyRange = dependencies[dependencyName];
166
+ if (typeof dependencyRange === 'string' && dependencyRange.trim().length > 0) {
167
+ addConstraint(expanded, dependencyName, dependencyRange);
168
+ }
169
+ }
170
+ }
171
+ return expanded;
172
+ }
173
+ function buildManifestIndex(manifests) {
174
+ const index = new Map();
175
+ for (const manifest of manifests) {
176
+ const bucket = index.get(manifest.name) ?? [];
177
+ bucket.push(manifest);
178
+ index.set(manifest.name, bucket);
179
+ }
180
+ for (const [name, bucket] of index) {
181
+ bucket.sort((a, b) => compareSemver(b.version, a.version));
182
+ index.set(name, bucket);
183
+ }
184
+ return index;
185
+ }
186
+ function findSolverConflict(constraints, manifestIndex) {
187
+ for (const packageName of [...constraints.keys()].sort()) {
188
+ const ranges = constraints.get(packageName) ?? [];
189
+ const candidates = manifestIndex.get(packageName) ?? [];
190
+ const available = candidates.map((candidate) => candidate.version);
191
+ const satisfiable = candidates.some((candidate) => ranges.every((range) => satisfiesRange(candidate.version, range)));
192
+ if (!satisfiable) {
193
+ return { packageName, ranges, available };
194
+ }
195
+ }
196
+ return null;
197
+ }
198
+ function solveSelection(manifestIndex, constraints, selected) {
199
+ const expanded = expandConstraintsWithDependencies(constraints, selected);
200
+ for (const [packageName, manifest] of selected) {
201
+ const ranges = expanded.get(packageName) ?? [];
202
+ if (!ranges.every((range) => satisfiesRange(manifest.version, range))) {
203
+ return null;
204
+ }
205
+ }
206
+ const unresolved = [...expanded.keys()]
207
+ .filter((packageName) => !selected.has(packageName))
208
+ .sort();
209
+ if (unresolved.length === 0) {
210
+ return selected;
211
+ }
212
+ const nextPackage = unresolved[0];
213
+ if (!nextPackage) {
214
+ return selected;
215
+ }
216
+ const ranges = expanded.get(nextPackage) ?? [];
217
+ const candidates = (manifestIndex.get(nextPackage) ?? []).filter((candidate) => ranges.every((range) => satisfiesRange(candidate.version, range)));
218
+ for (const candidate of candidates) {
219
+ const nextSelected = new Map(selected);
220
+ nextSelected.set(nextPackage, candidate);
221
+ const solved = solveSelection(manifestIndex, expanded, nextSelected);
222
+ if (solved) {
223
+ return solved;
224
+ }
225
+ }
226
+ return null;
227
+ }
228
+ function parseDependencyMap(value, fieldName) {
229
+ if (value === undefined) {
230
+ return {};
231
+ }
232
+ if (!isRecord(value)) {
233
+ throw new Error(`Invalid manifest field '${fieldName}'. Expected object map.`);
234
+ }
235
+ const dependencyMap = {};
236
+ for (const [dependencyName, dependencyRange] of Object.entries(value)) {
237
+ if (typeof dependencyRange !== 'string' || dependencyRange.trim().length === 0) {
238
+ throw new Error(`Invalid dependency range for '${dependencyName}' in '${fieldName}'.`);
239
+ }
240
+ dependencyMap[dependencyName] = dependencyRange.trim();
241
+ }
242
+ return sortedRecord(dependencyMap);
243
+ }
244
+ function parseRequiredTools(value) {
245
+ if (value === undefined) {
246
+ return [];
247
+ }
248
+ if (!Array.isArray(value)) {
249
+ throw new Error("Invalid manifest compatibility field 'requiredTools'.");
250
+ }
251
+ const tools = value.map((item, index) => asNonEmptyString(item, `compatibility.requiredTools[${index}]`));
252
+ return [...new Set(tools)].sort();
253
+ }
254
+ function parseCapabilities(value) {
255
+ if (value === undefined) {
256
+ return undefined;
257
+ }
258
+ if (!isRecord(value)) {
259
+ throw new Error("Invalid server field 'capabilities'.");
260
+ }
261
+ const parsed = {};
262
+ if (value.defaultScope !== undefined) {
263
+ if (typeof value.defaultScope !== 'string' ||
264
+ !CAPABILITY_SCOPES.has(value.defaultScope)) {
265
+ throw new Error("Invalid server capability 'defaultScope'.");
266
+ }
267
+ parsed.defaultScope = value.defaultScope;
268
+ }
269
+ if (value.tools !== undefined) {
270
+ if (!isRecord(value.tools)) {
271
+ throw new Error("Invalid server capability map 'tools'.");
272
+ }
273
+ const tools = {};
274
+ for (const [toolName, scopeValue] of Object.entries(value.tools)) {
275
+ if (typeof scopeValue !== 'string' ||
276
+ !CAPABILITY_SCOPES.has(scopeValue)) {
277
+ throw new Error(`Invalid capability scope for tool '${toolName}'.`);
278
+ }
279
+ tools[toolName] = scopeValue;
280
+ }
281
+ parsed.tools = sortedRecord(tools);
282
+ }
283
+ return parsed;
284
+ }
285
+ function parseServerConfig(value) {
286
+ if (!isRecord(value)) {
287
+ throw new Error("Invalid manifest field 'server'.");
288
+ }
289
+ const id = asNonEmptyString(value.id, 'server.id');
290
+ const name = asNonEmptyString(value.name, 'server.name');
291
+ const description = asNonEmptyString(value.description, 'server.description');
292
+ const transport = asNonEmptyString(value.transport, 'server.transport');
293
+ if (transport !== 'stdio' && transport !== 'sse') {
294
+ throw new Error(`Invalid server transport '${transport}'.`);
295
+ }
296
+ const command = asNonEmptyString(value.command, 'server.command');
297
+ if (!Array.isArray(value.args) || value.args.some((arg) => typeof arg !== 'string')) {
298
+ throw new Error("Invalid manifest field 'server.args'. Expected string array.");
299
+ }
300
+ let env;
301
+ if (value.env !== undefined) {
302
+ if (!isRecord(value.env)) {
303
+ throw new Error("Invalid manifest field 'server.env'.");
304
+ }
305
+ env = {};
306
+ for (const [envName, envValue] of Object.entries(value.env)) {
307
+ if (typeof envValue !== 'string') {
308
+ throw new Error(`Invalid env value for '${envName}' in server.env.`);
309
+ }
310
+ env[envName] = envValue;
311
+ }
312
+ env = sortedRecord(env);
313
+ }
314
+ return {
315
+ id,
316
+ name,
317
+ description,
318
+ transport,
319
+ command,
320
+ args: [...value.args],
321
+ env,
322
+ autoConnect: value.autoConnect === undefined ? undefined : Boolean(value.autoConnect),
323
+ enabled: value.enabled === undefined ? undefined : Boolean(value.enabled),
324
+ capabilities: parseCapabilities(value.capabilities),
325
+ };
326
+ }
327
+ function parseManifest(value) {
328
+ if (!isRecord(value)) {
329
+ throw new Error('Invalid package manifest entry.');
330
+ }
331
+ const packageName = asNonEmptyString(value.name, 'name');
332
+ const version = asNonEmptyString(value.version, 'version');
333
+ if (!parseSemver(version)) {
334
+ throw new Error(`Package '${packageName}' has invalid semver version '${version}'.`);
335
+ }
336
+ if (!isRecord(value.metadata)) {
337
+ throw new Error(`Package '${packageName}@${version}' is missing metadata.`);
338
+ }
339
+ const metadata = {
340
+ displayName: asNonEmptyString(value.metadata.displayName, 'metadata.displayName'),
341
+ description: asNonEmptyString(value.metadata.description, 'metadata.description'),
342
+ deprecated: value.metadata.deprecated === undefined ? undefined : Boolean(value.metadata.deprecated),
343
+ deprecationMessage: value.metadata.deprecationMessage === undefined
344
+ ? undefined
345
+ : asNonEmptyString(value.metadata.deprecationMessage, 'metadata.deprecationMessage'),
346
+ };
347
+ let compatibility;
348
+ if (value.compatibility !== undefined) {
349
+ if (!isRecord(value.compatibility)) {
350
+ throw new Error(`Package '${packageName}@${version}' has invalid compatibility block.`);
351
+ }
352
+ compatibility = {
353
+ node: value.compatibility.node === undefined
354
+ ? undefined
355
+ : asNonEmptyString(value.compatibility.node, 'compatibility.node'),
356
+ runtimeApi: value.compatibility.runtimeApi === undefined
357
+ ? undefined
358
+ : asNonEmptyString(value.compatibility.runtimeApi, 'compatibility.runtimeApi'),
359
+ requiredTools: parseRequiredTools(value.compatibility.requiredTools),
360
+ };
361
+ }
362
+ return {
363
+ name: packageName,
364
+ version,
365
+ metadata,
366
+ dependencies: parseDependencyMap(value.dependencies, 'dependencies'),
367
+ compatibility,
368
+ server: parseServerConfig(value.server),
369
+ };
370
+ }
371
+ function parseCatalog(rawContent) {
372
+ const parsed = JSON.parse(rawContent);
373
+ if (!isRecord(parsed) || !Array.isArray(parsed.packages)) {
374
+ throw new Error("Invalid package catalog. Expected object with 'packages' array.");
375
+ }
376
+ const manifests = parsed.packages.map((entry) => parseManifest(entry));
377
+ const seen = new Set();
378
+ for (const manifest of manifests) {
379
+ const key = `${manifest.name}@${manifest.version}`;
380
+ if (seen.has(key)) {
381
+ throw new Error(`Duplicate package manifest '${key}' in catalog.`);
382
+ }
383
+ seen.add(key);
384
+ }
385
+ manifests.sort((a, b) => {
386
+ const byName = a.name.localeCompare(b.name);
387
+ if (byName !== 0)
388
+ return byName;
389
+ return compareSemver(b.version, a.version);
390
+ });
391
+ return { packages: manifests };
392
+ }
393
+ function parseLockEntry(packageName, value) {
394
+ if (!isRecord(value)) {
395
+ throw new Error(`Invalid lockfile entry '${packageName}'.`);
396
+ }
397
+ const name = asNonEmptyString(value.name, `${packageName}.name`);
398
+ const version = asNonEmptyString(value.version, `${packageName}.version`);
399
+ if (!parseSemver(version)) {
400
+ throw new Error(`Invalid semver in lockfile entry '${packageName}'.`);
401
+ }
402
+ return {
403
+ name,
404
+ version,
405
+ dependencies: parseDependencyMap(value.dependencies, `${packageName}.dependencies`),
406
+ checksum: asNonEmptyString(value.checksum, `${packageName}.checksum`),
407
+ integrity: asNonEmptyString(value.integrity, `${packageName}.integrity`),
408
+ installedAt: asNonEmptyString(value.installedAt, `${packageName}.installedAt`),
409
+ };
410
+ }
411
+ function normalizeLockfile(lockfile) {
412
+ const normalizedPackages = {};
413
+ for (const packageName of Object.keys(lockfile.packages).sort()) {
414
+ const entry = lockfile.packages[packageName];
415
+ if (!entry)
416
+ continue;
417
+ normalizedPackages[packageName] = {
418
+ ...entry,
419
+ dependencies: sortedRecord(entry.dependencies),
420
+ };
421
+ }
422
+ return {
423
+ version: LOCKFILE_VERSION,
424
+ generatedAt: lockfile.generatedAt,
425
+ packages: normalizedPackages,
426
+ };
427
+ }
428
+ function emptyLockfile() {
429
+ return {
430
+ version: LOCKFILE_VERSION,
431
+ generatedAt: new Date(0).toISOString(),
432
+ packages: {},
433
+ };
434
+ }
435
+ function parseLockfile(rawContent) {
436
+ const parsed = JSON.parse(rawContent);
437
+ if (!isRecord(parsed)) {
438
+ throw new Error('Invalid lockfile root content.');
439
+ }
440
+ if (parsed.version !== LOCKFILE_VERSION) {
441
+ throw new Error(`Unsupported lockfile version '${String(parsed.version)}'. Expected ${LOCKFILE_VERSION}.`);
442
+ }
443
+ if (!isRecord(parsed.packages)) {
444
+ throw new Error("Invalid lockfile field 'packages'.");
445
+ }
446
+ const packages = {};
447
+ for (const [packageName, entryValue] of Object.entries(parsed.packages)) {
448
+ packages[packageName] = parseLockEntry(packageName, entryValue);
449
+ }
450
+ const generatedAt = asNonEmptyString(parsed.generatedAt, 'generatedAt');
451
+ return normalizeLockfile({
452
+ version: LOCKFILE_VERSION,
453
+ generatedAt,
454
+ packages,
455
+ });
456
+ }
457
+ export class SkillPackageManager {
458
+ #catalogPath;
459
+ #lockPath;
460
+ #runtimeApiVersion;
461
+ constructor(options = {}) {
462
+ this.#catalogPath = options.catalogPath ?? DEFAULT_CATALOG_PATH;
463
+ this.#lockPath = options.lockPath ?? DEFAULT_LOCK_PATH;
464
+ this.#runtimeApiVersion = options.runtimeApiVersion ?? DEFAULT_RUNTIME_API_VERSION;
465
+ }
466
+ async getActivationPlan() {
467
+ const [catalog, lockfile] = await Promise.all([
468
+ this.#readCatalog(),
469
+ this.#readLockfile(),
470
+ ]);
471
+ return this.#buildActivationPlan(catalog, lockfile);
472
+ }
473
+ async getDiagnostics() {
474
+ const plan = await this.getActivationPlan();
475
+ return plan.diagnostics;
476
+ }
477
+ async installPackage(packageName, versionRange) {
478
+ const targetName = packageName.trim();
479
+ if (!targetName) {
480
+ throw new Error('Package name is required for install.');
481
+ }
482
+ const [catalog, currentLock] = await Promise.all([
483
+ this.#readCatalog(),
484
+ this.#readLockfile(),
485
+ ]);
486
+ const targetEntry = currentLock.packages[targetName];
487
+ const effectiveRange = versionRange?.trim() ??
488
+ (targetEntry ? `=${targetEntry.version}` : '*');
489
+ const selected = this.#solveSelection(catalog, currentLock, targetName, effectiveRange);
490
+ this.#assertCompatibility(selected);
491
+ const nextLock = this.#buildLockfileFromSelection(selected, currentLock);
492
+ const changed = this.#serializePackagesOnly(currentLock) !== this.#serializePackagesOnly(nextLock);
493
+ await this.#persistLockfileWithRollback(currentLock, nextLock);
494
+ const diagnostics = await this.getDiagnostics();
495
+ return {
496
+ action: 'install',
497
+ packageName: targetName,
498
+ version: selected.get(targetName)?.version,
499
+ changed,
500
+ diagnostics,
501
+ };
502
+ }
503
+ async upgradePackage(packageName, versionRange) {
504
+ const targetName = packageName.trim();
505
+ if (!targetName) {
506
+ throw new Error('Package name is required for upgrade.');
507
+ }
508
+ const [catalog, currentLock] = await Promise.all([
509
+ this.#readCatalog(),
510
+ this.#readLockfile(),
511
+ ]);
512
+ const selected = this.#solveSelection(catalog, currentLock, targetName, versionRange?.trim() || '*');
513
+ this.#assertCompatibility(selected);
514
+ const nextLock = this.#buildLockfileFromSelection(selected, currentLock);
515
+ const changed = this.#serializePackagesOnly(currentLock) !== this.#serializePackagesOnly(nextLock);
516
+ await this.#persistLockfileWithRollback(currentLock, nextLock);
517
+ const diagnostics = await this.getDiagnostics();
518
+ return {
519
+ action: 'upgrade',
520
+ packageName: targetName,
521
+ version: selected.get(targetName)?.version,
522
+ changed,
523
+ diagnostics,
524
+ };
525
+ }
526
+ async uninstallPackage(packageName) {
527
+ const targetName = packageName.trim();
528
+ if (!targetName) {
529
+ throw new Error('Package name is required for uninstall.');
530
+ }
531
+ const currentLock = await this.#readLockfile();
532
+ const targetEntry = currentLock.packages[targetName];
533
+ if (!targetEntry) {
534
+ throw new Error(`Package '${targetName}' is not installed.`);
535
+ }
536
+ const dependents = Object.values(currentLock.packages)
537
+ .filter((entry) => entry.name !== targetName && entry.dependencies[targetName] !== undefined)
538
+ .map((entry) => `${entry.name}@${entry.version}`);
539
+ if (dependents.length > 0) {
540
+ throw new Error(`Cannot uninstall '${targetName}'. Depended on by: ${dependents.join(', ')}.`);
541
+ }
542
+ const nextPackages = { ...currentLock.packages };
543
+ delete nextPackages[targetName];
544
+ const nextLock = normalizeLockfile({
545
+ version: LOCKFILE_VERSION,
546
+ generatedAt: new Date().toISOString(),
547
+ packages: nextPackages,
548
+ });
549
+ const changed = this.#serializePackagesOnly(currentLock) !== this.#serializePackagesOnly(nextLock);
550
+ await this.#persistLockfileWithRollback(currentLock, nextLock);
551
+ const diagnostics = await this.getDiagnostics();
552
+ return {
553
+ action: 'uninstall',
554
+ packageName: targetName,
555
+ changed,
556
+ diagnostics,
557
+ };
558
+ }
559
+ #solveSelection(catalog, currentLock, targetName, targetRange) {
560
+ const manifestIndex = buildManifestIndex(catalog.packages);
561
+ const constraints = new Map();
562
+ for (const entry of Object.values(currentLock.packages)) {
563
+ if (entry.name !== targetName) {
564
+ addConstraint(constraints, entry.name, `=${entry.version}`);
565
+ }
566
+ }
567
+ addConstraint(constraints, targetName, targetRange);
568
+ const solved = solveSelection(manifestIndex, constraints, new Map());
569
+ if (solved) {
570
+ return solved;
571
+ }
572
+ // Expand constraints greedily (pick best candidate per package) to expose transitive conflicts
573
+ const tentativeSelected = new Map();
574
+ for (const [name, ranges] of constraints) {
575
+ const best = (manifestIndex.get(name) ?? []).find((c) => ranges.every((r) => satisfiesRange(c.version, r)));
576
+ if (best) {
577
+ tentativeSelected.set(name, best);
578
+ }
579
+ }
580
+ const expandedConstraints = expandConstraintsWithDependencies(constraints, tentativeSelected);
581
+ const conflict = findSolverConflict(expandedConstraints, manifestIndex);
582
+ if (conflict) {
583
+ const availableText = conflict.available.length > 0 ? conflict.available.join(', ') : '(none)';
584
+ throw new Error(`No compatible version found for '${conflict.packageName}' with constraints [${conflict.ranges.join(', ')}]. Available versions: ${availableText}.`);
585
+ }
586
+ throw new Error(`Failed to resolve package graph for '${targetName}'.`);
587
+ }
588
+ #assertCompatibility(selection) {
589
+ const violations = [];
590
+ const selectionByName = new Map(selection);
591
+ const sortedSelection = [...selection.values()].sort((a, b) => a.name.localeCompare(b.name));
592
+ for (const manifest of sortedSelection) {
593
+ const packageViolations = this.#evaluateManifestCompatibility(manifest);
594
+ violations.push(...packageViolations);
595
+ const dependencies = manifest.dependencies ?? {};
596
+ for (const dependencyName of Object.keys(dependencies).sort()) {
597
+ const dependencyRange = dependencies[dependencyName];
598
+ const selectedDependency = selectionByName.get(dependencyName);
599
+ if (!selectedDependency) {
600
+ violations.push(this.#buildViolation(manifest.name, manifest.version, 'missing-dependency', `Dependency '${dependencyName}' is missing from the resolved graph.`, `Add '${dependencyName}' to the catalog and satisfy range '${dependencyRange}'.`));
601
+ continue;
602
+ }
603
+ if (!satisfiesRange(selectedDependency.version, dependencyRange)) {
604
+ violations.push(this.#buildViolation(manifest.name, manifest.version, 'version-conflict', `Dependency '${dependencyName}@${selectedDependency.version}' does not satisfy '${dependencyRange}'.`, `Adjust version constraints for '${manifest.name}' or '${dependencyName}'.`));
605
+ }
606
+ }
607
+ }
608
+ if (violations.length > 0) {
609
+ const message = violations
610
+ .map((violation) => `[${violation.code}] ${violation.packageName}@${violation.version}: ${violation.message} (${violation.remediation})`)
611
+ .join(' | ');
612
+ throw new Error(`Compatibility gate blocked activation: ${message}`);
613
+ }
614
+ }
615
+ #evaluateManifestCompatibility(manifest) {
616
+ const violations = [];
617
+ const compatibility = manifest.compatibility;
618
+ if (!compatibility) {
619
+ return violations;
620
+ }
621
+ if (compatibility.node && !satisfiesRange(process.version.replace(/^v/, ''), compatibility.node)) {
622
+ violations.push(this.#buildViolation(manifest.name, manifest.version, 'node-incompatible', `Runtime Node.js ${process.version} does not satisfy '${compatibility.node}'.`, `Use a Node.js version that matches '${compatibility.node}'.`));
623
+ }
624
+ if (compatibility.runtimeApi &&
625
+ !satisfiesRange(this.#runtimeApiVersion, compatibility.runtimeApi)) {
626
+ violations.push(this.#buildViolation(manifest.name, manifest.version, 'runtime-api-incompatible', `Runtime API ${this.#runtimeApiVersion} does not satisfy '${compatibility.runtimeApi}'.`, `Install a compatible package version or upgrade runtime API.`));
627
+ }
628
+ const requiredTools = compatibility.requiredTools ?? [];
629
+ for (const toolName of requiredTools) {
630
+ if (!this.#commandExists(toolName)) {
631
+ violations.push(this.#buildViolation(manifest.name, manifest.version, 'missing-required-tool', `Required executable '${toolName}' is not available on PATH.`, `Install '${toolName}' or remove this package.`));
632
+ }
633
+ }
634
+ return violations;
635
+ }
636
+ #buildActivationPlan(catalog, lockfile) {
637
+ const manifestByKey = new Map();
638
+ for (const manifest of catalog.packages) {
639
+ manifestByKey.set(`${manifest.name}@${manifest.version}`, manifest);
640
+ }
641
+ const installedEntries = Object.values(lockfile.packages).sort((a, b) => a.name.localeCompare(b.name));
642
+ const warnings = [];
643
+ const violations = [];
644
+ const servers = [];
645
+ const seenServerIds = new Set();
646
+ for (const entry of installedEntries) {
647
+ const manifest = manifestByKey.get(`${entry.name}@${entry.version}`);
648
+ if (!manifest) {
649
+ violations.push(this.#buildViolation(entry.name, entry.version, 'missing-manifest', `Installed package '${entry.name}@${entry.version}' is missing from catalog.`, `Reinstall '${entry.name}' from an available version or update the catalog.`));
650
+ continue;
651
+ }
652
+ if (manifest.metadata.deprecated) {
653
+ warnings.push(manifest.metadata.deprecationMessage ??
654
+ `Package '${entry.name}@${entry.version}' is deprecated.`);
655
+ }
656
+ const compatibilityViolations = this.#evaluateManifestCompatibility(manifest);
657
+ violations.push(...compatibilityViolations);
658
+ let dependencyConflict = false;
659
+ for (const [dependencyName, dependencyRange] of Object.entries(entry.dependencies)) {
660
+ const installedDependency = lockfile.packages[dependencyName];
661
+ if (!installedDependency) {
662
+ dependencyConflict = true;
663
+ violations.push(this.#buildViolation(entry.name, entry.version, 'missing-dependency', `Dependency '${dependencyName}' is not installed.`, `Install '${dependencyName}' with range '${dependencyRange}'.`));
664
+ continue;
665
+ }
666
+ if (!satisfiesRange(installedDependency.version, dependencyRange)) {
667
+ dependencyConflict = true;
668
+ violations.push(this.#buildViolation(entry.name, entry.version, 'version-conflict', `Dependency '${dependencyName}@${installedDependency.version}' violates '${dependencyRange}'.`, `Upgrade or downgrade '${dependencyName}' to satisfy '${dependencyRange}'.`));
669
+ }
670
+ }
671
+ if (compatibilityViolations.length === 0 &&
672
+ !dependencyConflict &&
673
+ !seenServerIds.has(manifest.server.id)) {
674
+ seenServerIds.add(manifest.server.id);
675
+ servers.push(manifest.server);
676
+ continue;
677
+ }
678
+ if (seenServerIds.has(manifest.server.id)) {
679
+ violations.push(this.#buildViolation(entry.name, entry.version, 'duplicate-server-id', `Server id '${manifest.server.id}' is already provided by another active package.`, `Uninstall one package that provides '${manifest.server.id}'.`));
680
+ }
681
+ }
682
+ return {
683
+ servers,
684
+ diagnostics: {
685
+ installed: installedEntries,
686
+ warnings,
687
+ violations,
688
+ activePackageCount: servers.length,
689
+ blockedPackageCount: Math.max(installedEntries.length - servers.length, 0),
690
+ },
691
+ };
692
+ }
693
+ #commandExists(commandName) {
694
+ const result = spawnSync('where', [commandName], { stdio: 'ignore' });
695
+ return result.status === 0;
696
+ }
697
+ #buildViolation(packageName, version, code, message, remediation) {
698
+ return {
699
+ packageName,
700
+ version,
701
+ code,
702
+ message,
703
+ remediation,
704
+ };
705
+ }
706
+ #buildLockfileFromSelection(selection, currentLock) {
707
+ const now = new Date().toISOString();
708
+ const packages = {};
709
+ const manifests = [...selection.values()].sort((a, b) => a.name.localeCompare(b.name));
710
+ for (const manifest of manifests) {
711
+ const existing = currentLock.packages[manifest.name];
712
+ const checksum = this.#manifestChecksum(manifest);
713
+ packages[manifest.name] = {
714
+ name: manifest.name,
715
+ version: manifest.version,
716
+ dependencies: sortedRecord(manifest.dependencies ?? {}),
717
+ checksum,
718
+ integrity: `sha256-${checksum}`,
719
+ installedAt: existing && existing.version === manifest.version ? existing.installedAt : now,
720
+ };
721
+ }
722
+ return normalizeLockfile({
723
+ version: LOCKFILE_VERSION,
724
+ generatedAt: this.#serializePackagesOnly({ version: LOCKFILE_VERSION, generatedAt: '', packages }) ===
725
+ this.#serializePackagesOnly(currentLock)
726
+ ? currentLock.generatedAt
727
+ : now,
728
+ packages,
729
+ });
730
+ }
731
+ #manifestChecksum(manifest) {
732
+ const stableManifest = {
733
+ ...manifest,
734
+ dependencies: sortedRecord(manifest.dependencies ?? {}),
735
+ compatibility: manifest.compatibility
736
+ ? {
737
+ ...manifest.compatibility,
738
+ requiredTools: [...(manifest.compatibility.requiredTools ?? [])].sort(),
739
+ }
740
+ : undefined,
741
+ server: {
742
+ ...manifest.server,
743
+ args: [...manifest.server.args],
744
+ env: manifest.server.env ? sortedRecord(manifest.server.env) : undefined,
745
+ capabilities: manifest.server.capabilities
746
+ ? {
747
+ ...manifest.server.capabilities,
748
+ tools: manifest.server.capabilities.tools
749
+ ? sortedRecord(manifest.server.capabilities.tools)
750
+ : undefined,
751
+ }
752
+ : undefined,
753
+ },
754
+ };
755
+ return createHash('sha256')
756
+ .update(JSON.stringify(stableManifest))
757
+ .digest('hex');
758
+ }
759
+ async #persistLockfileWithRollback(previousLock, nextLock) {
760
+ const snapshot = this.#serializeLockfile(previousLock);
761
+ try {
762
+ await this.#writeLockfile(nextLock);
763
+ }
764
+ catch (error) {
765
+ await this.#restoreLockSnapshot(snapshot);
766
+ const message = error instanceof Error ? error.message : String(error);
767
+ throw new Error(`Lockfile update failed; rollback applied. ${message}`);
768
+ }
769
+ }
770
+ async #restoreLockSnapshot(snapshot) {
771
+ const tempPath = `${this.#lockPath}.rollback`;
772
+ await writeFile(tempPath, snapshot, 'utf8');
773
+ await rename(tempPath, this.#lockPath);
774
+ }
775
+ async #writeLockfile(lockfile) {
776
+ const normalized = normalizeLockfile(lockfile);
777
+ const serialized = this.#serializeLockfile(normalized);
778
+ const tempPath = `${this.#lockPath}.tmp`;
779
+ await writeFile(tempPath, serialized, 'utf8');
780
+ await rename(tempPath, this.#lockPath);
781
+ }
782
+ #serializeLockfile(lockfile) {
783
+ return `${JSON.stringify(normalizeLockfile(lockfile), null, 2)}\n`;
784
+ }
785
+ #serializePackagesOnly(lockfile) {
786
+ return JSON.stringify(normalizeLockfile(lockfile).packages);
787
+ }
788
+ async #readCatalog() {
789
+ await this.#ensureFileExists(this.#catalogPath, `${JSON.stringify({ packages: [] }, null, 2)}\n`);
790
+ const content = await readFile(this.#catalogPath, 'utf8');
791
+ return parseCatalog(content);
792
+ }
793
+ async #readLockfile() {
794
+ await this.#ensureFileExists(this.#lockPath, this.#serializeLockfile(emptyLockfile()));
795
+ const content = await readFile(this.#lockPath, 'utf8');
796
+ return parseLockfile(content);
797
+ }
798
+ async #ensureFileExists(filePath, defaultContent) {
799
+ try {
800
+ await access(filePath);
801
+ }
802
+ catch {
803
+ await writeFile(filePath, defaultContent, 'utf8');
804
+ }
805
+ }
806
+ }