@oddessentials/agent-guild 1.28.0 → 1.30.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 (133) hide show
  1. package/bin/native/win32-arm64/agent-guild-report.exe +0 -0
  2. package/bin/native/win32-x64/agent-guild-report.exe +0 -0
  3. package/bin/windows/arm64/agent-guild-start.exe +0 -0
  4. package/bin/windows/x64/agent-guild-start.exe +0 -0
  5. package/docs/api.md +10 -2
  6. package/node_modules/@jsquash/jpeg/CHANGELOG.md +54 -0
  7. package/node_modules/@jsquash/jpeg/LICENSE +201 -0
  8. package/node_modules/@jsquash/jpeg/README.md +105 -0
  9. package/node_modules/@jsquash/jpeg/codec/LICENSE.codec.md +132 -0
  10. package/node_modules/@jsquash/jpeg/codec/dec/mozjpeg_dec.d.ts +7 -0
  11. package/node_modules/@jsquash/jpeg/codec/dec/mozjpeg_dec.js +15 -0
  12. package/node_modules/@jsquash/jpeg/codec/dec/mozjpeg_dec.wasm +0 -0
  13. package/node_modules/@jsquash/jpeg/codec/enc/mozjpeg_enc.d.ts +37 -0
  14. package/node_modules/@jsquash/jpeg/codec/enc/mozjpeg_enc.js +15 -0
  15. package/node_modules/@jsquash/jpeg/codec/enc/mozjpeg_enc.wasm +0 -0
  16. package/node_modules/@jsquash/jpeg/codec/pre.js +24 -0
  17. package/node_modules/@jsquash/jpeg/decode.d.ts +16 -0
  18. package/node_modules/@jsquash/jpeg/decode.js +36 -0
  19. package/node_modules/@jsquash/jpeg/emscripten-types.d.ts +137 -0
  20. package/node_modules/@jsquash/jpeg/encode.d.ts +21 -0
  21. package/node_modules/@jsquash/jpeg/encode.js +35 -0
  22. package/node_modules/@jsquash/jpeg/index.d.ts +3 -0
  23. package/node_modules/@jsquash/jpeg/index.js +2 -0
  24. package/node_modules/@jsquash/jpeg/meta.d.ts +24 -0
  25. package/node_modules/@jsquash/jpeg/meta.js +25 -0
  26. package/node_modules/@jsquash/jpeg/package.json +32 -0
  27. package/node_modules/@jsquash/jpeg/utils.d.ts +17 -0
  28. package/node_modules/@jsquash/jpeg/utils.js +31 -0
  29. package/node_modules/@jsquash/png/CHANGELOG.md +69 -0
  30. package/node_modules/@jsquash/png/LICENSE +201 -0
  31. package/node_modules/@jsquash/png/README.md +114 -0
  32. package/node_modules/@jsquash/png/codec/LICENSE.codec.md +29 -0
  33. package/node_modules/@jsquash/png/codec/README.md +5 -0
  34. package/node_modules/@jsquash/png/codec/pkg/README.md +5 -0
  35. package/node_modules/@jsquash/png/codec/pkg/squoosh_png.d.ts +71 -0
  36. package/node_modules/@jsquash/png/codec/pkg/squoosh_png.js +314 -0
  37. package/node_modules/@jsquash/png/codec/pkg/squoosh_png_bg.wasm +0 -0
  38. package/node_modules/@jsquash/png/codec/pkg/squoosh_png_bg.wasm.d.ts +13 -0
  39. package/node_modules/@jsquash/png/codec/pre.js +24 -0
  40. package/node_modules/@jsquash/png/decode.d.ts +29 -0
  41. package/node_modules/@jsquash/png/decode.js +35 -0
  42. package/node_modules/@jsquash/png/encode.d.ts +31 -0
  43. package/node_modules/@jsquash/png/encode.js +40 -0
  44. package/node_modules/@jsquash/png/index.d.ts +3 -0
  45. package/node_modules/@jsquash/png/index.js +2 -0
  46. package/node_modules/@jsquash/png/meta.d.ts +16 -0
  47. package/node_modules/@jsquash/png/meta.js +15 -0
  48. package/node_modules/@jsquash/png/package.json +34 -0
  49. package/node_modules/@jsquash/webp/CHANGELOG.md +48 -0
  50. package/node_modules/@jsquash/webp/LICENSE +201 -0
  51. package/node_modules/@jsquash/webp/README.md +100 -0
  52. package/node_modules/@jsquash/webp/codec/LICENSE.codec.md +29 -0
  53. package/node_modules/@jsquash/webp/codec/Makefile.animated.backup +88 -0
  54. package/node_modules/@jsquash/webp/codec/README.md +5 -0
  55. package/node_modules/@jsquash/webp/codec/dec/README.md +18 -0
  56. package/node_modules/@jsquash/webp/codec/dec/webp_dec.d.ts +7 -0
  57. package/node_modules/@jsquash/webp/codec/dec/webp_dec.js +16 -0
  58. package/node_modules/@jsquash/webp/codec/dec/webp_dec.wasm +0 -0
  59. package/node_modules/@jsquash/webp/codec/enc/README.md +22 -0
  60. package/node_modules/@jsquash/webp/codec/enc/webp_enc.d.ts +42 -0
  61. package/node_modules/@jsquash/webp/codec/enc/webp_enc.js +16 -0
  62. package/node_modules/@jsquash/webp/codec/enc/webp_enc.wasm +0 -0
  63. package/node_modules/@jsquash/webp/codec/enc/webp_enc_simd.d.ts +1 -0
  64. package/node_modules/@jsquash/webp/codec/enc/webp_enc_simd.js +16 -0
  65. package/node_modules/@jsquash/webp/codec/enc/webp_enc_simd.wasm +0 -0
  66. package/node_modules/@jsquash/webp/codec/pre.js +24 -0
  67. package/node_modules/@jsquash/webp/decode.d.ts +15 -0
  68. package/node_modules/@jsquash/webp/decode.js +34 -0
  69. package/node_modules/@jsquash/webp/emscripten-types.d.ts +137 -0
  70. package/node_modules/@jsquash/webp/encode.d.ts +23 -0
  71. package/node_modules/@jsquash/webp/encode.js +43 -0
  72. package/node_modules/@jsquash/webp/index.d.ts +3 -0
  73. package/node_modules/@jsquash/webp/index.js +2 -0
  74. package/node_modules/@jsquash/webp/meta.d.ts +19 -0
  75. package/node_modules/@jsquash/webp/meta.js +33 -0
  76. package/node_modules/@jsquash/webp/package.json +35 -0
  77. package/node_modules/@jsquash/webp/utils.d.ts +17 -0
  78. package/node_modules/@jsquash/webp/utils.js +31 -0
  79. package/node_modules/argparse/lib/argparse.js +1 -1
  80. package/node_modules/argparse/lib/textwrap.js +13 -2
  81. package/node_modules/argparse/package.json +1 -1
  82. package/node_modules/markdown-it/dist/browser/markdown-it.esm.min.mjs +737 -686
  83. package/node_modules/markdown-it/dist/browser/markdown-it.esm.min.mjs.map +1 -1
  84. package/node_modules/markdown-it/dist/browser/markdown-it.umd.min.js +8 -8
  85. package/node_modules/markdown-it/dist/browser/markdown-it.umd.min.js.map +1 -1
  86. package/node_modules/markdown-it/dist/markdown-it.cjs.js +660 -518
  87. package/node_modules/markdown-it/dist/markdown-it.cjs.js.map +1 -1
  88. package/node_modules/markdown-it/dist/markdown-it.d.mts +1 -0
  89. package/node_modules/markdown-it/dist/markdown-it.mjs +179 -124
  90. package/node_modules/markdown-it/dist/markdown-it.mjs.map +1 -1
  91. package/node_modules/markdown-it/package.json +11 -11
  92. package/node_modules/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
  93. package/node_modules/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
  94. package/node_modules/node-pty/prebuilds/win32-x64/conpty.node +0 -0
  95. package/node_modules/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
  96. package/node_modules/wasm-feature-detect/LICENSE +202 -0
  97. package/node_modules/wasm-feature-detect/README.md +109 -0
  98. package/node_modules/wasm-feature-detect/README.md.ejs +89 -0
  99. package/node_modules/wasm-feature-detect/dist/cjs/index.cjs +1 -0
  100. package/node_modules/wasm-feature-detect/dist/esm/index.js +1 -0
  101. package/node_modules/wasm-feature-detect/dist/index.d.cts +24 -0
  102. package/node_modules/wasm-feature-detect/dist/index.d.ts +24 -0
  103. package/node_modules/wasm-feature-detect/dist/umd/index.js +1 -0
  104. package/node_modules/wasm-feature-detect/package.json +53 -0
  105. package/node_modules/ws/README.md +21 -29
  106. package/node_modules/ws/lib/buffer-util.js +13 -2
  107. package/node_modules/ws/lib/sender.js +4 -1
  108. package/node_modules/ws/lib/websocket.js +8 -3
  109. package/node_modules/ws/package.json +1 -1
  110. package/package.json +7 -4
  111. package/src/manager/config.mjs +2 -0
  112. package/src/manager/folder-browser.mjs +39 -3
  113. package/src/manager/main.mjs +4 -0
  114. package/src/manager/server.mjs +32 -5
  115. package/src/manager/steam-workshop.mjs +64 -0
  116. package/src/manager/yard-art.mjs +1 -1
  117. package/src/manager/yard-piece-check.mjs +424 -0
  118. package/src/manager/yard-pieces.mjs +188 -0
  119. package/src/manager/yard-worlds.mjs +214 -0
  120. package/web/app.js +175 -11
  121. package/web/index.html +20 -2
  122. package/web/skins/grove/ui/companion-bird.webp +0 -0
  123. package/web/skins/guild/skin.css +2 -13
  124. package/web/styles.css +15 -1
  125. package/web/yard/art.json +1 -1
  126. package/web/yard/model.mjs +44 -2
  127. package/web/yard/photo.js +3 -4
  128. package/web/yard/renderer.js +150 -38
  129. package/web/yard/vendor/engine.js +500 -495
  130. package/web/yard/view.js +160 -14
  131. package/web/yard/yard.css +55 -12
  132. package/web/skins/grove/ui/install-now-dark.svg +0 -91
  133. package/web/skins/grove/ui/install-now-light.svg +0 -91
@@ -56,39 +56,27 @@ can use one of the many wrappers available on npm, like
56
56
  npm install ws
57
57
  ```
58
58
 
59
- ### Opt-in for performance
59
+ ### Legacy opt-in for performance
60
60
 
61
- [bufferutil][] is an optional module that can be installed alongside the ws
62
- module:
61
+ On older Node.js versions, ws can leverage optional binary modules to optimize
62
+ performance.
63
63
 
64
- ```
65
- npm install --save-optional bufferutil
66
- ```
67
-
68
- This is a binary addon that improves the performance of certain operations such
69
- as masking and unmasking the data payload of the WebSocket frames. Prebuilt
70
- binaries are available for the most popular platforms, so you don't necessarily
71
- need to have a C++ compiler installed on your machine.
72
-
73
- To force ws to not use bufferutil, use the
74
- [`WS_NO_BUFFER_UTIL`](./doc/ws.md#ws_no_buffer_util) environment variable. This
75
- can be useful to enhance security in systems where a user can put a package in
76
- the package search path of an application of another user, due to how the
77
- Node.js resolver algorithm works.
78
-
79
- #### Legacy opt-in for performance
80
-
81
- If you are running on an old version of Node.js (prior to v18.14.0), ws also
82
- supports the [utf-8-validate][] module:
83
-
84
- ```
85
- npm install --save-optional utf-8-validate
86
- ```
64
+ - `npm install --save-optional bufferutil`: Provides binary polyfills for
65
+ [`http.websocketMask()`][] and [`http.websocketUnmask()`][].
66
+ - `npm install --save-optional utf-8-validate`: Provides a binary polyfill for
67
+ [`buffer.isUtf8()`][].
87
68
 
88
- This contains a binary polyfill for [`buffer.isUtf8()`][].
69
+ On newer Node.js versions (v27.0.0-alpha.0.0.0 or later for bufferutil, and
70
+ v18.14.0 or later for utf-8-validate) that feature built-in alternatives, these
71
+ modules are not loaded even if installed.
89
72
 
90
- To force ws not to use utf-8-validate, use the
91
- [`WS_NO_UTF_8_VALIDATE`](./doc/ws.md#ws_no_utf_8_validate) environment variable.
73
+ To force ws not to use [bufferutil][] or [utf-8-validate][] on the older Node.js
74
+ versions where they would otherwise be loaded, set the
75
+ [`WS_NO_BUFFER_UTIL`](./doc/ws.md#ws_no_buffer_util) and
76
+ [`WS_NO_UTF_8_VALIDATE`](./doc/ws.md#ws_no_utf_8_validate) environment
77
+ variables. This can be useful to enhance security in systems where a user can
78
+ put a package in the package search path of an application of another user, due
79
+ to how the Node.js resolver algorithm works.
92
80
 
93
81
  ## API docs
94
82
 
@@ -533,6 +521,10 @@ We're using the GitHub [releases][changelog] for changelog entries.
533
521
  [MIT](LICENSE)
534
522
 
535
523
  [`buffer.isutf8()`]: https://nodejs.org/api/buffer.html#bufferisutf8input
524
+ [`http.websocketMask()`]:
525
+ https://nodejs.org/api/http.html#httpwebsocketmasksource-mask-output-offset-length
526
+ [`http.websocketUnmask()`]:
527
+ https://nodejs.org/api/http.html#httpwebsocketunmaskbuffer-mask
536
528
  [bufferutil]: https://github.com/websockets/bufferutil
537
529
  [changelog]: https://github.com/websockets/ws/releases
538
530
  [client-report]: http://websockets.github.io/ws/autobahn/clients/
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { websocketMask, websocketUnmask } = require('http');
4
+
3
5
  const { EMPTY_BUFFER } = require('./constants');
4
6
 
5
7
  const FastBuffer = Buffer[Symbol.species];
@@ -111,8 +113,17 @@ module.exports = {
111
113
  unmask: _unmask
112
114
  };
113
115
 
114
- /* istanbul ignore else */
115
- if (!process.env.WS_NO_BUFFER_UTIL) {
116
+ if (websocketMask) {
117
+ module.exports.mask = function (source, mask, output, offset, length) {
118
+ if (length < 24) _mask(source, mask, output, offset, length);
119
+ else websocketMask(source, mask, output, offset, length);
120
+ };
121
+
122
+ module.exports.unmask = function (buffer, mask) {
123
+ if (buffer.length < 24) _unmask(buffer, mask);
124
+ else websocketUnmask(buffer, mask);
125
+ };
126
+ } /* istanbul ignore else */ else if (!process.env.WS_NO_BUFFER_UTIL) {
116
127
  try {
117
128
  const bufferUtil = require('bufferutil');
118
129
 
@@ -153,7 +153,9 @@ class Sender {
153
153
  target.writeUIntBE(dataLength, 4, 6);
154
154
  }
155
155
 
156
- if (!options.mask) return [target, data];
156
+ if (!options.mask) {
157
+ return dataLength ? [target, data] : [target];
158
+ }
157
159
 
158
160
  target[1] |= 0x80;
159
161
  target[offset - 4] = mask[0];
@@ -161,6 +163,7 @@ class Sender {
161
163
  target[offset - 2] = mask[2];
162
164
  target[offset - 1] = mask[3];
163
165
 
166
+ if (!dataLength) return [target];
164
167
  if (skipMasking) return [target, data];
165
168
 
166
169
  if (merge) {
@@ -819,10 +819,15 @@ function initAsClient(websocket, address, protocols, options) {
819
819
  }
820
820
 
821
821
  if (isIpcUrl) {
822
- const parts = opts.path.split(':');
822
+ const i = opts.path.indexOf(':');
823
823
 
824
- opts.socketPath = parts[0];
825
- opts.path = parts[1];
824
+ if (i === -1) {
825
+ opts.socketPath = opts.path;
826
+ opts.path = '';
827
+ } else {
828
+ opts.socketPath = opts.path.slice(0, i);
829
+ opts.path = opts.path.slice(i + 1);
830
+ }
826
831
  }
827
832
 
828
833
  let req;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ws",
3
- "version": "8.22.0",
3
+ "version": "8.22.1",
4
4
  "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js",
5
5
  "keywords": [
6
6
  "HyBi",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oddessentials/agent-guild",
3
- "version": "1.28.0",
3
+ "version": "1.30.0",
4
4
  "description": "Launch and watch AI coding-assistant terminal sessions from one local web page. A bundled session manager owns the terminals so the page can close and reconnect.",
5
5
  "mcpName": "ai.oddessentials/agent-guild",
6
6
  "license": "AGPL-3.0-only",
@@ -50,17 +50,20 @@
50
50
  "test": "node --test --test-concurrency=1 \"tests/*.test.mjs\""
51
51
  },
52
52
  "dependencies": {
53
+ "@jsquash/jpeg": "1.6.0",
54
+ "@jsquash/png": "3.1.1",
55
+ "@jsquash/webp": "1.5.0",
53
56
  "@noble/hashes": "2.4.0",
54
57
  "@xterm/addon-fit": "^0.11.0",
55
58
  "@xterm/addon-serialize": "^0.14.0",
56
59
  "@xterm/addon-web-links": "^0.12.0",
57
60
  "@xterm/headless": "^6.0.0",
58
61
  "@xterm/xterm": "^6.0.0",
59
- "markdown-it": "^15.0.2",
62
+ "markdown-it": "^15.0.3",
60
63
  "node-pty": "^1.2.0-beta.15",
61
64
  "qrcode-generator": "2.0.4",
62
65
  "smol-toml": "1.9.1",
63
- "ws": "^8.22.0"
66
+ "ws": "^8.22.1"
64
67
  },
65
68
  "bundleDependencies": true,
66
69
  "publishConfig": {
@@ -74,6 +77,6 @@
74
77
  "@gltf-transform/extensions": "4.5.1",
75
78
  "@gltf-transform/functions": "4.5.1",
76
79
  "esbuild": "0.28.2",
77
- "three": "0.186.1"
80
+ "three": "0.187.0"
78
81
  }
79
82
  }
@@ -56,6 +56,8 @@ export const paths = {
56
56
  get multiplexers() { return path.join(dataDir(), 'multiplexers.json'); },
57
57
  /** The Yard's art packs, one folder per content id. */
58
58
  get yardArt() { return path.join(dataDir(), 'yard-art'); },
59
+ get yardWorlds() { return path.join(dataDir(), 'yard-worlds'); },
60
+ get yardPieces() { return path.join(dataDir(), 'yard-pieces'); },
59
61
  };
60
62
 
61
63
  function writePrivate(file, contents) {
@@ -1,8 +1,10 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
+ import { readManifest } from './yard-pieces.mjs';
4
5
 
5
6
  const failure = (status, code, message) => Object.assign(new Error(message), { status, code });
7
+ const PIECE_SCAN = 500;
6
8
  const byName = (a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base', numeric: true }) || (a.name < b.name ? -1 : 1);
7
9
 
8
10
  export function folderSegments(dir, paths = path) {
@@ -30,7 +32,7 @@ export function folderNameProblem(name, platform = process.platform) {
30
32
  return null;
31
33
  }
32
34
 
33
- export function createFolderBrowser({ platform = process.platform, home = os.homedir(), limit = 2000, driveTimeoutMs = 500, fsp = fs.promises } = {}) {
35
+ export function createFolderBrowser({ platform = process.platform, home = os.homedir(), limit = 2000, driveTimeoutMs = 500, fsp = fs.promises, workshop = null } = {}) {
34
36
  const paths = platform === 'win32' ? path.win32 : path.posix;
35
37
 
36
38
  async function roots() {
@@ -73,6 +75,37 @@ export function createFolderBrowser({ platform = process.platform, home = os.hom
73
75
  return folders.filter(Boolean).sort(byName);
74
76
  }
75
77
 
78
+ async function filesIn(dir, extension) {
79
+ const listed = await fsp.readdir(dir, { withFileTypes: true }).catch(() => []);
80
+ return listed.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(extension))
81
+ .map((entry) => ({ name: entry.name, path: paths.join(dir, entry.name) })).sort(byName);
82
+ }
83
+
84
+ async function pieceIn(dir, kind) {
85
+ const file = paths.join(dir, 'piece.json');
86
+ try {
87
+ const stat = await fsp.lstat(file);
88
+ if (!stat.isFile() || stat.size > 16 * 1024) return null;
89
+ const bytes = await fsp.readFile(file), folder = paths.basename(dir);
90
+ try {
91
+ const { title } = readManifest(bytes, kind);
92
+ return { title: title || folder, folder, path: dir };
93
+ } catch (err) {
94
+ const meta = err.newer ? JSON.parse(bytes.toString('utf8')) : null;
95
+ if (meta?.kind !== kind) return null;
96
+ const title = typeof meta.title === 'string' && meta.title.length <= 128 && !/\p{Cc}/u.test(meta.title) ? meta.title.trim() : '';
97
+ return { title: title || folder, folder, path: dir, problem: err.message };
98
+ }
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ async function piecesIn(dir, folders, kind) {
105
+ const found = await Promise.all([dir, ...folders.slice(0, PIECE_SCAN).map((entry) => entry.path)].map((folder) => pieceIn(folder, kind)));
106
+ return found.filter(Boolean).sort((a, b) => a.title.localeCompare(b.title, undefined, { sensitivity: 'base', numeric: true }) || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
107
+ }
108
+
76
109
  const browser = {
77
110
  async create(rawParent, rawName) {
78
111
  if (typeof rawParent !== 'string' || !rawParent.trim() || rawParent.includes('\0')) throw failure(400, 'bad_path', 'The folder must be a path.');
@@ -97,7 +130,7 @@ export function createFolderBrowser({ platform = process.platform, home = os.hom
97
130
  }
98
131
  return browser.list(dir);
99
132
  },
100
- async list(raw) {
133
+ async list(raw, { extension = null, pieces = null } = {}) {
101
134
  if (raw !== undefined && raw !== null && (typeof raw !== 'string' || raw.includes('\0'))) {
102
135
  throw failure(400, 'bad_path', 'The folder must be a path.');
103
136
  }
@@ -105,8 +138,9 @@ export function createFolderBrowser({ platform = process.platform, home = os.hom
105
138
  if (asked === '~' || asked.startsWith('~/') || asked.startsWith('~\\')) asked = paths.join(home, asked.slice(1));
106
139
  const wanted = asked ? paths.resolve(asked) : home;
107
140
  const dir = await nearestFolder(wanted);
108
- const [folders, drives] = await Promise.all([entries(dir), roots()]);
141
+ const [folders, drives, files] = await Promise.all([entries(dir), roots(), extension ? filesIn(dir, extension) : null]);
109
142
  const parent = paths.dirname(dir);
143
+ const [found, steam] = pieces ? await Promise.all([piecesIn(dir, folders, pieces), workshop ? workshop() : null]) : [null, null];
110
144
  return {
111
145
  path: dir,
112
146
  parent: parent === dir ? null : parent,
@@ -116,6 +150,8 @@ export function createFolderBrowser({ platform = process.platform, home = os.hom
116
150
  entries: folders.slice(0, limit),
117
151
  truncated: folders.length > limit,
118
152
  note: dir === wanted ? null : wanted,
153
+ ...(files ? { files: files.slice(0, limit) } : {}),
154
+ ...(found ? { pieces: found, workshop: steam } : {}),
119
155
  };
120
156
  },
121
157
  };
@@ -17,6 +17,8 @@ import { Changelog } from './changelog.mjs';
17
17
  import { GitHub } from './github.mjs';
18
18
  import { createManagerServer } from './server.mjs';
19
19
  import { YardArt } from './yard-art.mjs';
20
+ import { YardWorlds } from './yard-worlds.mjs';
21
+ import { YardPieces } from './yard-pieces.mjs';
20
22
  import { loadPty } from './pty.mjs';
21
23
  import { createNotesStore } from './notes.mjs';
22
24
  import { createCardNames } from './card-names.mjs';
@@ -186,6 +188,8 @@ export async function startManager({ port = resolvePort(), host = DEFAULT_HOST,
186
188
  notes: createNotesStore(paths.notes),
187
189
  launcher: launcherPath(),
188
190
  yardArt: new YardArt({ webDir, dir: paths.yardArt }),
191
+ yardWorlds: new YardWorlds({ dir: paths.yardWorlds }),
192
+ yardPieces: new YardPieces({ dir: paths.yardPieces }),
189
193
  mcpConnect,
190
194
  onShutdownRequest: ({ restart = false } = {}) => shutdown('requested via API', { restart }).then((code) => process.exit(code)),
191
195
  });
@@ -19,6 +19,9 @@ import { createOptionalCards } from './optional-cards.mjs';
19
19
  import { MODES as AUTOSTART_MODES } from './autostart.mjs';
20
20
  import { ptyRestartProblem } from './pty.mjs';
21
21
  import { YardArt } from './yard-art.mjs';
22
+ import { YardWorlds } from './yard-worlds.mjs';
23
+ import { YardPieces } from './yard-pieces.mjs';
24
+ import { createWorkshopFinder } from './steam-workshop.mjs';
22
25
  import { spokenText } from './spoken-text.mjs';
23
26
  import { MCP_ACTIONS } from './mcp-connect.mjs';
24
27
 
@@ -69,8 +72,8 @@ const SECURITY_HEADERS = {
69
72
  'Referrer-Policy': 'no-referrer',
70
73
  'Cross-Origin-Resource-Policy': 'same-origin',
71
74
  'Content-Security-Policy':
72
- "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://i.ytimg.com; " +
73
- "connect-src 'self' ws://127.0.0.1:* ws://localhost:*; frame-src https://www.youtube-nocookie.com; " +
75
+ "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://i.ytimg.com; " +
76
+ "connect-src 'self' blob: ws://127.0.0.1:* ws://localhost:*; frame-src https://www.youtube-nocookie.com; " +
74
77
  "frame-ancestors vscode-webview: vscode-file:; base-uri 'none'; form-action 'none'",
75
78
  };
76
79
 
@@ -176,7 +179,7 @@ export function createManagerServer({
176
179
  notes = createNotesStore(),
177
180
  deviceSignIn = new DeviceSignIn({ token }),
178
181
  folderOpener = createFolderOpener({ resolveCwd: (cwd) => manager.resolveCwd(cwd) }),
179
- folderBrowser = createFolderBrowser(),
182
+ folderBrowser = createFolderBrowser({ workshop: createWorkshopFinder() }),
180
183
  /** The double-click launcher file for this platform, or null when the package carries none. */
181
184
  launcher = null,
182
185
  /** @type {(opts: { restart: boolean }) => void} */
@@ -185,6 +188,8 @@ export function createManagerServer({
185
188
  nextManagerProblem = ptyRestartProblem,
186
189
  /** The Yard's art; without a data folder, only a checkout's. */
187
190
  yardArt = new YardArt({ webDir }),
191
+ yardWorlds = new YardWorlds(),
192
+ yardPieces = new YardPieces(),
188
193
  mcpConnect = null,
189
194
  heartbeatMs = 30000,
190
195
  }) {
@@ -292,6 +297,18 @@ export function createManagerServer({
292
297
  throw new HttpError(400, 'malformed path', 'bad_path');
293
298
  }
294
299
  if (rel.includes('\0')) throw new HttpError(404, 'not found', 'not_found');
300
+ if (rel.startsWith('yard/worlds/')) {
301
+ const [skin, name, ...rest] = rel.slice('yard/worlds/'.length).split('/');
302
+ const world = rest.length ? null : yardWorlds.file(skin, name);
303
+ if (!world) throw new HttpError(404, 'not found', 'not_found');
304
+ return serveFile(req, res, world);
305
+ }
306
+ if (rel.startsWith('yard/pieces/')) {
307
+ const [skin, slot, name, ...rest] = rel.slice('yard/pieces/'.length).split('/');
308
+ const piece = rest.length ? null : yardPieces.file(skin, slot, name);
309
+ if (!piece) throw new HttpError(404, 'not found', 'not_found');
310
+ return serveFile(req, res, piece);
311
+ }
295
312
  let root = webDir;
296
313
  if (rel.startsWith('yard/assets/')) {
297
314
  root = yardArt.assetsDir();
@@ -408,6 +425,14 @@ export function createManagerServer({
408
425
  if (route === '/yard-art' && method === 'GET') return sendJson(res, 200, { yardArt: yardArt.snapshot() });
409
426
  if (route === '/yard-art/download' && method === 'POST') return sendJson(res, 202, { yardArt: yardArt.start() });
410
427
  if (route === '/yard-art/cancel' && method === 'POST') return sendJson(res, 200, { yardArt: yardArt.cancel() });
428
+ if (route === '/yard-worlds' && method === 'GET') return sendJson(res, 200, { yardWorlds: yardWorlds.snapshot() });
429
+ const yardWorld = route.match(/^\/yard-worlds\/([a-z]+)$/);
430
+ if (yardWorld && method === 'PUT') return sendJson(res, 200, { yardWorlds: await yardWorlds.replace(yardWorld[1], (await readJsonBody(req)).path) });
431
+ if (yardWorld && method === 'DELETE') return sendJson(res, 200, { yardWorlds: await yardWorlds.restore(yardWorld[1]) });
432
+ if (route === '/yard-pieces' && method === 'GET') return sendJson(res, 200, { yardPieces: yardPieces.snapshot() });
433
+ const yardPiece = route.match(/^\/yard-pieces\/([a-z]+)\/([a-z]+)$/);
434
+ if (yardPiece && method === 'PUT') return sendJson(res, 200, { yardPieces: await yardPieces.replace(yardPiece[1], yardPiece[2], (await readJsonBody(req)).path) });
435
+ if (yardPiece && method === 'DELETE') return sendJson(res, 200, { yardPieces: await yardPieces.restore(yardPiece[1], yardPiece[2]) });
411
436
  if (route === '/yard/fireworks' && method === 'POST') {
412
437
  broadcast({ type: 'yard.fireworks' });
413
438
  return sendJson(res, 200, { ok: true, pages: [...eventClients].filter((ws) => !ws.revoked && ws.readyState === ws.OPEN).length });
@@ -435,7 +460,7 @@ export function createManagerServer({
435
460
  return sendJson(res, 200, { ok: true });
436
461
  }
437
462
  if (route === '/folders' && method === 'GET') {
438
- return sendJson(res, 200, await folderBrowser.list(url.searchParams.get('path') ?? undefined));
463
+ return sendJson(res, 200, await folderBrowser.list(url.searchParams.get('path') ?? undefined, { extension: url.searchParams.get('files') === 'agworld' ? '.agworld' : null, pieces: ['character', 'companion'].includes(url.searchParams.get('pieces')) ? url.searchParams.get('pieces') : null }));
439
464
  }
440
465
  if (route === '/folders' && method === 'POST') {
441
466
  const { path: parent, name } = await readJsonBody(req);
@@ -771,10 +796,12 @@ export function createManagerServer({
771
796
  remoteAccess?.on('updated', () => broadcast({ type: 'remote-access.updated' }));
772
797
  deviceSignIn.on('updated', () => broadcast({ type: 'sign-in.updated' }));
773
798
  yardArt.on('updated', (status) => broadcast({ type: 'yard-art.updated', yardArt: status }));
799
+ yardWorlds.on('updated', (worlds) => broadcast({ type: 'yard-worlds.updated', yardWorlds: worlds }));
800
+ yardPieces.on('updated', (pieces) => broadcast({ type: 'yard-pieces.updated', yardPieces: pieces }));
774
801
 
775
802
  function handleEvents(ws, req) {
776
803
  eventClients.add(ws);
777
- const hello = { type: 'hello', version, pid: process.pid, platform: process.platform, startedAt, launcher, folderOpener: folderOpenerFor(req), remoteAccess: remoteAccess ? { available: true } : null, upgrade: upgradeInfo(), yardArt: yardArt.snapshot(), sessions: manager.list(), heartbeat: true };
804
+ const hello = { type: 'hello', version, pid: process.pid, platform: process.platform, startedAt, launcher, folderOpener: folderOpenerFor(req), remoteAccess: remoteAccess ? { available: true } : null, upgrade: upgradeInfo(), yardArt: yardArt.snapshot(), yardWorlds: yardWorlds.snapshot(), yardPieces: yardPieces.snapshot(), sessions: manager.list(), heartbeat: true };
778
805
  const notesHello = notes.helloRevision();
779
806
  if (notesHello.known) hello.notesRevision = notesHello.revision;
780
807
  else hello.notesUnreadable = true;
@@ -0,0 +1,64 @@
1
+ import { execFile } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ export const WORLD_APP = '5462970';
7
+
8
+ export function parseVdf(text) {
9
+ const tokens = text.match(/"(?:[^"\\]|\\.)*"|[{}]/g) ?? [];
10
+ const root = {}, stack = [root];
11
+ let key = null;
12
+ for (const token of tokens) {
13
+ const top = stack[stack.length - 1];
14
+ if (token === '{') {
15
+ if (key === null) return root;
16
+ stack.push(top[key.toLowerCase()] = {});
17
+ key = null;
18
+ } else if (token === '}') {
19
+ if (stack.length > 1) stack.pop();
20
+ key = null;
21
+ } else {
22
+ const value = token.slice(1, -1).replace(/\\(.)/g, '$1');
23
+ if (key === null) key = value;
24
+ else { top[key.toLowerCase()] = value; key = null; }
25
+ }
26
+ }
27
+ return root;
28
+ }
29
+
30
+ function windowsSteam() {
31
+ return new Promise((resolve) => {
32
+ execFile('reg', ['query', 'HKCU\\Software\\Valve\\Steam', '/v', 'SteamPath'], { timeout: 5000, windowsHide: true }, (err, stdout) => {
33
+ resolve(err ? null : String(stdout).match(/SteamPath\s+REG_\w+\s+(.+)/)?.[1]?.trim() || null);
34
+ });
35
+ });
36
+ }
37
+
38
+ export function steamRoots({ platform = process.platform, home = os.homedir(), registry = windowsSteam } = {}) {
39
+ if (platform === 'win32') return registry().then((root) => (root ? [root] : []));
40
+ if (platform === 'darwin') return Promise.resolve([path.join(home, 'Library', 'Application Support', 'Steam')]);
41
+ return Promise.resolve([path.join(home, '.local', 'share', 'Steam'), path.join(home, '.steam', 'steam'), path.join(home, '.var', 'app', 'com.valvesoftware.Steam', '.local', 'share', 'Steam')]);
42
+ }
43
+
44
+ export async function worldLibrary({ roots = steamRoots(), fsp = fs.promises } = {}) {
45
+ for (const root of await roots) {
46
+ let folders;
47
+ try { folders = parseVdf(await fsp.readFile(path.join(root, 'steamapps', 'libraryfolders.vdf'), 'utf8')).libraryfolders; } catch { continue; }
48
+ for (const entry of Object.values(folders ?? {})) {
49
+ if (typeof entry === 'object' && typeof entry.path === 'string' && entry.apps && Object.hasOwn(entry.apps, WORLD_APP)) return path.normalize(entry.path);
50
+ }
51
+ }
52
+ return null;
53
+ }
54
+
55
+ export function createWorkshopFinder({ library = () => worldLibrary(), fsp = fs.promises } = {}) {
56
+ let found = null;
57
+ return async () => {
58
+ found ??= library().catch(() => null);
59
+ const root = await found;
60
+ if (!root) { found = null; return null; }
61
+ const folder = path.join(root, 'steamapps', 'workshop', 'content', WORLD_APP);
62
+ return (await fsp.stat(folder).then((stat) => stat.isDirectory(), () => false)) ? folder : null;
63
+ };
64
+ }
@@ -283,7 +283,7 @@ export class YardArt extends EventEmitter {
283
283
  }
284
284
 
285
285
  // Windows can hold a just-written folder briefly (indexing, antivirus).
286
- async function renameRetrying(from, to) {
286
+ export async function renameRetrying(from, to) {
287
287
  for (let attempt = 0; ; attempt++) {
288
288
  try { return await fsp.rename(from, to); } catch (err) {
289
289
  if (attempt >= 20 || !['EPERM', 'EBUSY', 'EACCES'].includes(err.code)) throw err;