@oddessentials/agent-guild 1.29.0 → 1.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) 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 +6 -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/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
  80. package/node_modules/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
  81. package/node_modules/node-pty/prebuilds/win32-x64/conpty.node +0 -0
  82. package/node_modules/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
  83. package/node_modules/wasm-feature-detect/LICENSE +202 -0
  84. package/node_modules/wasm-feature-detect/README.md +109 -0
  85. package/node_modules/wasm-feature-detect/README.md.ejs +89 -0
  86. package/node_modules/wasm-feature-detect/dist/cjs/index.cjs +1 -0
  87. package/node_modules/wasm-feature-detect/dist/esm/index.js +1 -0
  88. package/node_modules/wasm-feature-detect/dist/index.d.cts +24 -0
  89. package/node_modules/wasm-feature-detect/dist/index.d.ts +24 -0
  90. package/node_modules/wasm-feature-detect/dist/umd/index.js +1 -0
  91. package/node_modules/wasm-feature-detect/package.json +53 -0
  92. package/package.json +4 -1
  93. package/src/manager/config.mjs +1 -0
  94. package/src/manager/folder-browser.mjs +31 -2
  95. package/src/manager/main.mjs +2 -0
  96. package/src/manager/server.mjs +19 -5
  97. package/src/manager/steam-workshop.mjs +64 -0
  98. package/src/manager/yard-piece-check.mjs +424 -0
  99. package/src/manager/yard-pieces.mjs +188 -0
  100. package/web/app.js +55 -17
  101. package/web/index.html +14 -1
  102. package/web/skins/guild/skin.css +2 -13
  103. package/web/styles.css +14 -0
  104. package/web/yard/art.json +1 -1
  105. package/web/yard/model.mjs +44 -2
  106. package/web/yard/renderer.js +136 -27
  107. package/web/yard/view.js +123 -11
@@ -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() {
@@ -79,6 +81,31 @@ export function createFolderBrowser({ platform = process.platform, home = os.hom
79
81
  .map((entry) => ({ name: entry.name, path: paths.join(dir, entry.name) })).sort(byName);
80
82
  }
81
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
+
82
109
  const browser = {
83
110
  async create(rawParent, rawName) {
84
111
  if (typeof rawParent !== 'string' || !rawParent.trim() || rawParent.includes('\0')) throw failure(400, 'bad_path', 'The folder must be a path.');
@@ -103,7 +130,7 @@ export function createFolderBrowser({ platform = process.platform, home = os.hom
103
130
  }
104
131
  return browser.list(dir);
105
132
  },
106
- async list(raw, { extension = null } = {}) {
133
+ async list(raw, { extension = null, pieces = null } = {}) {
107
134
  if (raw !== undefined && raw !== null && (typeof raw !== 'string' || raw.includes('\0'))) {
108
135
  throw failure(400, 'bad_path', 'The folder must be a path.');
109
136
  }
@@ -113,6 +140,7 @@ export function createFolderBrowser({ platform = process.platform, home = os.hom
113
140
  const dir = await nearestFolder(wanted);
114
141
  const [folders, drives, files] = await Promise.all([entries(dir), roots(), extension ? filesIn(dir, extension) : null]);
115
142
  const parent = paths.dirname(dir);
143
+ const [found, steam] = pieces ? await Promise.all([piecesIn(dir, folders, pieces), workshop ? workshop() : null]) : [null, null];
116
144
  return {
117
145
  path: dir,
118
146
  parent: parent === dir ? null : parent,
@@ -123,6 +151,7 @@ export function createFolderBrowser({ platform = process.platform, home = os.hom
123
151
  truncated: folders.length > limit,
124
152
  note: dir === wanted ? null : wanted,
125
153
  ...(files ? { files: files.slice(0, limit) } : {}),
154
+ ...(found ? { pieces: found, workshop: steam } : {}),
126
155
  };
127
156
  },
128
157
  };
@@ -18,6 +18,7 @@ import { GitHub } from './github.mjs';
18
18
  import { createManagerServer } from './server.mjs';
19
19
  import { YardArt } from './yard-art.mjs';
20
20
  import { YardWorlds } from './yard-worlds.mjs';
21
+ import { YardPieces } from './yard-pieces.mjs';
21
22
  import { loadPty } from './pty.mjs';
22
23
  import { createNotesStore } from './notes.mjs';
23
24
  import { createCardNames } from './card-names.mjs';
@@ -188,6 +189,7 @@ export async function startManager({ port = resolvePort(), host = DEFAULT_HOST,
188
189
  launcher: launcherPath(),
189
190
  yardArt: new YardArt({ webDir, dir: paths.yardArt }),
190
191
  yardWorlds: new YardWorlds({ dir: paths.yardWorlds }),
192
+ yardPieces: new YardPieces({ dir: paths.yardPieces }),
191
193
  mcpConnect,
192
194
  onShutdownRequest: ({ restart = false } = {}) => shutdown('requested via API', { restart }).then((code) => process.exit(code)),
193
195
  });
@@ -20,6 +20,8 @@ import { MODES as AUTOSTART_MODES } from './autostart.mjs';
20
20
  import { ptyRestartProblem } from './pty.mjs';
21
21
  import { YardArt } from './yard-art.mjs';
22
22
  import { YardWorlds } from './yard-worlds.mjs';
23
+ import { YardPieces } from './yard-pieces.mjs';
24
+ import { createWorkshopFinder } from './steam-workshop.mjs';
23
25
  import { spokenText } from './spoken-text.mjs';
24
26
  import { MCP_ACTIONS } from './mcp-connect.mjs';
25
27
 
@@ -70,8 +72,8 @@ const SECURITY_HEADERS = {
70
72
  'Referrer-Policy': 'no-referrer',
71
73
  'Cross-Origin-Resource-Policy': 'same-origin',
72
74
  'Content-Security-Policy':
73
- "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://i.ytimg.com; " +
74
- "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; " +
75
77
  "frame-ancestors vscode-webview: vscode-file:; base-uri 'none'; form-action 'none'",
76
78
  };
77
79
 
@@ -177,7 +179,7 @@ export function createManagerServer({
177
179
  notes = createNotesStore(),
178
180
  deviceSignIn = new DeviceSignIn({ token }),
179
181
  folderOpener = createFolderOpener({ resolveCwd: (cwd) => manager.resolveCwd(cwd) }),
180
- folderBrowser = createFolderBrowser(),
182
+ folderBrowser = createFolderBrowser({ workshop: createWorkshopFinder() }),
181
183
  /** The double-click launcher file for this platform, or null when the package carries none. */
182
184
  launcher = null,
183
185
  /** @type {(opts: { restart: boolean }) => void} */
@@ -187,6 +189,7 @@ export function createManagerServer({
187
189
  /** The Yard's art; without a data folder, only a checkout's. */
188
190
  yardArt = new YardArt({ webDir }),
189
191
  yardWorlds = new YardWorlds(),
192
+ yardPieces = new YardPieces(),
190
193
  mcpConnect = null,
191
194
  heartbeatMs = 30000,
192
195
  }) {
@@ -300,6 +303,12 @@ export function createManagerServer({
300
303
  if (!world) throw new HttpError(404, 'not found', 'not_found');
301
304
  return serveFile(req, res, world);
302
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
+ }
303
312
  let root = webDir;
304
313
  if (rel.startsWith('yard/assets/')) {
305
314
  root = yardArt.assetsDir();
@@ -420,6 +429,10 @@ export function createManagerServer({
420
429
  const yardWorld = route.match(/^\/yard-worlds\/([a-z]+)$/);
421
430
  if (yardWorld && method === 'PUT') return sendJson(res, 200, { yardWorlds: await yardWorlds.replace(yardWorld[1], (await readJsonBody(req)).path) });
422
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]) });
423
436
  if (route === '/yard/fireworks' && method === 'POST') {
424
437
  broadcast({ type: 'yard.fireworks' });
425
438
  return sendJson(res, 200, { ok: true, pages: [...eventClients].filter((ws) => !ws.revoked && ws.readyState === ws.OPEN).length });
@@ -447,7 +460,7 @@ export function createManagerServer({
447
460
  return sendJson(res, 200, { ok: true });
448
461
  }
449
462
  if (route === '/folders' && method === 'GET') {
450
- return sendJson(res, 200, await folderBrowser.list(url.searchParams.get('path') ?? undefined, { extension: url.searchParams.get('files') === 'agworld' ? '.agworld' : null }));
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 }));
451
464
  }
452
465
  if (route === '/folders' && method === 'POST') {
453
466
  const { path: parent, name } = await readJsonBody(req);
@@ -784,10 +797,11 @@ export function createManagerServer({
784
797
  deviceSignIn.on('updated', () => broadcast({ type: 'sign-in.updated' }));
785
798
  yardArt.on('updated', (status) => broadcast({ type: 'yard-art.updated', yardArt: status }));
786
799
  yardWorlds.on('updated', (worlds) => broadcast({ type: 'yard-worlds.updated', yardWorlds: worlds }));
800
+ yardPieces.on('updated', (pieces) => broadcast({ type: 'yard-pieces.updated', yardPieces: pieces }));
787
801
 
788
802
  function handleEvents(ws, req) {
789
803
  eventClients.add(ws);
790
- 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(), 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 };
791
805
  const notesHello = notes.helloRevision();
792
806
  if (notesHello.known) hello.notesRevision = notesHello.revision;
793
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
+ }