@vgai/sdk 0.5.41 → 0.5.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/play/log-format-node.ts +35 -0
- package/src/play/log-format.ts +0 -29
- package/src/play/log-operations.ts +2 -1
- package/src/project/build-discipline.ts +7 -1
- package/src/project/session-journal.ts +33 -0
- package/src/project-tool-catalog.ts +3 -1
- package/src/registry.ts +1 -1
- package/src/types.ts +10 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@vgai/sdk",
|
|
3
3
|
"author": "Volter AI, Inc.",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"version": "0.5.
|
|
5
|
+
"version": "0.5.44",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
34
|
-
"@vgai/engine": "0.5.
|
|
34
|
+
"@vgai/engine": "0.5.44",
|
|
35
35
|
"playwright": "^1.58.2",
|
|
36
36
|
"zod": "^4.3.6"
|
|
37
37
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Node half of the play-log format: the header fast-read over a file
|
|
3
|
+
* descriptor. Split from `log-format.ts` so the browser tier (which writes
|
|
4
|
+
* and parses the same format through the StorageBackend) can import the
|
|
5
|
+
* format without `node:fs` — the same split `game-globals-prelude.ts`
|
|
6
|
+
* established for its two programs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { closeSync, openSync, readSync } from 'node:fs';
|
|
10
|
+
import { asPlayLogHeader, type PlayLogHeader } from './log-format.js';
|
|
11
|
+
|
|
12
|
+
const HEADER_READ_BYTES = 4096;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The header of the log file at `path`, read WITHOUT loading the file — a
|
|
16
|
+
* listing walks every log in `logs/`, and those files are unbounded (a long
|
|
17
|
+
* run writes megabytes). `null` for an unreadable file or one with no header.
|
|
18
|
+
*/
|
|
19
|
+
export function readPlayLogHeaderFile(path: string): PlayLogHeader | null {
|
|
20
|
+
let fd: number | null = null;
|
|
21
|
+
try {
|
|
22
|
+
fd = openSync(path, 'r');
|
|
23
|
+
const buffer = Buffer.alloc(HEADER_READ_BYTES);
|
|
24
|
+
const bytes = readSync(fd, buffer, 0, HEADER_READ_BYTES, 0);
|
|
25
|
+
const head = buffer.subarray(0, bytes).toString('utf-8');
|
|
26
|
+
const newline = head.indexOf('\n');
|
|
27
|
+
const line = newline === -1 ? head : head.slice(0, newline);
|
|
28
|
+
if (line.trim() === '') return null;
|
|
29
|
+
return asPlayLogHeader(JSON.parse(line));
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
} finally {
|
|
33
|
+
if (fd !== null) closeSync(fd);
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/play/log-format.ts
CHANGED
|
@@ -26,8 +26,6 @@
|
|
|
26
26
|
* "no header" means "this run's identity was never recorded", never an error.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
import { closeSync, openSync, readSync } from 'node:fs';
|
|
30
|
-
|
|
31
29
|
/** `kind` value marking a play log's header line. */
|
|
32
30
|
export const PLAY_LOG_HEADER_KIND = 'session';
|
|
33
31
|
|
|
@@ -76,30 +74,3 @@ export function asPlayLogHeader(record: unknown): PlayLogHeader | null {
|
|
|
76
74
|
startedAt: typeof rec['startedAt'] === 'number' ? rec['startedAt'] : 0,
|
|
77
75
|
};
|
|
78
76
|
}
|
|
79
|
-
|
|
80
|
-
/** How much of a log file's start is read to find its header. A header line is
|
|
81
|
-
* a few hundred bytes; anything longer is not one. */
|
|
82
|
-
const HEADER_READ_BYTES = 4096;
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* The header of the log file at `path`, read WITHOUT loading the file — a
|
|
86
|
-
* listing walks every log in `logs/`, and those files are unbounded (a long
|
|
87
|
-
* run writes megabytes). `null` for an unreadable file or one with no header.
|
|
88
|
-
*/
|
|
89
|
-
export function readPlayLogHeaderFile(path: string): PlayLogHeader | null {
|
|
90
|
-
let fd: number | null = null;
|
|
91
|
-
try {
|
|
92
|
-
fd = openSync(path, 'r');
|
|
93
|
-
const buffer = Buffer.alloc(HEADER_READ_BYTES);
|
|
94
|
-
const bytes = readSync(fd, buffer, 0, HEADER_READ_BYTES, 0);
|
|
95
|
-
const head = buffer.subarray(0, bytes).toString('utf-8');
|
|
96
|
-
const newline = head.indexOf('\n');
|
|
97
|
-
const line = newline === -1 ? head : head.slice(0, newline);
|
|
98
|
-
if (line.trim() === '') return null;
|
|
99
|
-
return asPlayLogHeader(JSON.parse(line));
|
|
100
|
-
} catch {
|
|
101
|
-
return null;
|
|
102
|
-
} finally {
|
|
103
|
-
if (fd !== null) closeSync(fd);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
@@ -45,7 +45,8 @@ import {
|
|
|
45
45
|
resolveProjectPath,
|
|
46
46
|
} from '../project/shared.js';
|
|
47
47
|
import { defineTool, type ToolRegistry } from '../registry.js';
|
|
48
|
-
import { asPlayLogHeader, type PlayLogHeader
|
|
48
|
+
import { asPlayLogHeader, type PlayLogHeader } from './log-format.js';
|
|
49
|
+
import { readPlayLogHeaderFile } from './log-format-node.js';
|
|
49
50
|
import {
|
|
50
51
|
getPlayTransport,
|
|
51
52
|
PLAY_READ_TIMEOUT_MS,
|
|
@@ -459,7 +459,12 @@ export function unplayedSessionTier(
|
|
|
459
459
|
sessionStartedAtMs: number | null,
|
|
460
460
|
newestEvidence: number | null,
|
|
461
461
|
now: number,
|
|
462
|
+
/** Does the project declare anything to play? A MODELS project (no roots)
|
|
463
|
+
* has no game to run; the nag sent one to `vgai play`, which refused,
|
|
464
|
+
* and the refusal sat on the console (blind lantern round, 2026-09-06). */
|
|
465
|
+
playable = true,
|
|
462
466
|
): TripwireTier {
|
|
467
|
+
if (!playable) return 'silent';
|
|
463
468
|
if (sessionStartedAtMs === null) return 'silent';
|
|
464
469
|
if (newestEvidence !== null && newestEvidence >= sessionStartedAtMs) return 'silent';
|
|
465
470
|
const servingForMs = now - sessionStartedAtMs;
|
|
@@ -482,8 +487,9 @@ export function unplayedSessionBanner(
|
|
|
482
487
|
sessionStartedAtMs: number | null,
|
|
483
488
|
newestEvidence: number | null,
|
|
484
489
|
now: number,
|
|
490
|
+
playable = true,
|
|
485
491
|
): string | null {
|
|
486
|
-
const tier = unplayedSessionTier(sessionStartedAtMs, newestEvidence, now);
|
|
492
|
+
const tier = unplayedSessionTier(sessionStartedAtMs, newestEvidence, now, playable);
|
|
487
493
|
if (tier === 'silent' || sessionStartedAtMs === null) return null;
|
|
488
494
|
const elapsed = formatElapsed(now - sessionStartedAtMs);
|
|
489
495
|
if (tier === 'notice') {
|
|
@@ -290,6 +290,22 @@ export type SessionJournalEvent =
|
|
|
290
290
|
| { readonly kind: 'tab-gap-closed'; readonly tabId8: string; readonly gapMs: number }
|
|
291
291
|
/** Same tabId, new epoch: the page reloaded. The tab never left. */
|
|
292
292
|
| { readonly kind: 'tab-reloaded'; readonly tabId8: string; readonly epochCount: number }
|
|
293
|
+
/** The page announced main-thread work before starting it (a play-boot
|
|
294
|
+
* step, a model's `building src/models/x.ts`), or cleared it (`null`). */
|
|
295
|
+
| { readonly kind: 'page-phase'; readonly phase: string | null }
|
|
296
|
+
/**
|
|
297
|
+
* What the project's source watcher saw: one line per chokidar event under
|
|
298
|
+
* `src/` (`add`/`change`/`unlink`), with the file's mtime as the watcher
|
|
299
|
+
* found it (`null` when the path was gone). The instrument for "why did
|
|
300
|
+
* the page reload / the table refresh" — a reload on a file whose mtime
|
|
301
|
+
* never moved is a phantom event, and this is where it shows.
|
|
302
|
+
*/
|
|
303
|
+
| {
|
|
304
|
+
readonly kind: 'src-watch';
|
|
305
|
+
readonly event: string;
|
|
306
|
+
readonly path: string;
|
|
307
|
+
readonly mtimeMs: number | null;
|
|
308
|
+
}
|
|
293
309
|
/** Absent past its grace: this tab is gone. */
|
|
294
310
|
| { readonly kind: 'tab-departed'; readonly tabId8: string; readonly absentMs: number }
|
|
295
311
|
/**
|
|
@@ -392,6 +408,17 @@ export type SessionJournalEvent =
|
|
|
392
408
|
readonly by: string;
|
|
393
409
|
readonly reason: string;
|
|
394
410
|
readonly message: string;
|
|
411
|
+
}
|
|
412
|
+
/** A condition cleared by its OWNER, which proved it gone by an event the
|
|
413
|
+
* reload rule cannot see — a play remount resolving its own
|
|
414
|
+
* `Restart required` warning. `by` names that owner, never a person. */
|
|
415
|
+
| {
|
|
416
|
+
readonly kind: 'console-resolved';
|
|
417
|
+
readonly id: string;
|
|
418
|
+
readonly severity: 'error' | 'warn';
|
|
419
|
+
readonly count: number;
|
|
420
|
+
readonly by: string;
|
|
421
|
+
readonly message: string;
|
|
395
422
|
};
|
|
396
423
|
|
|
397
424
|
/** A parsed journal line: the event plus when it was appended. */
|
|
@@ -631,6 +658,10 @@ export function formatJournalLine(line: SessionJournalLine): string {
|
|
|
631
658
|
return `journal: ${at} tab-gap-closed ${line.tabId8} after ${Math.round(line.gapMs / 100) / 10}s`;
|
|
632
659
|
case 'tab-reloaded':
|
|
633
660
|
return `journal: ${at} tab-reloaded ${line.tabId8} (page load ${line.epochCount})`;
|
|
661
|
+
case 'page-phase':
|
|
662
|
+
return `journal: ${at} page-phase ${line.phase ?? '(clear)'}`;
|
|
663
|
+
case 'src-watch':
|
|
664
|
+
return `journal: ${at} src-watch ${line.event} ${line.path}${line.mtimeMs === null ? ' (gone)' : ''}`;
|
|
634
665
|
case 'tab-departed':
|
|
635
666
|
return `journal: ${at} tab-departed ${line.tabId8} absent ${Math.round(line.absentMs / 100) / 10}s`;
|
|
636
667
|
case 'tab-duplicated':
|
|
@@ -652,6 +683,8 @@ export function formatJournalLine(line: SessionJournalLine): string {
|
|
|
652
683
|
return `journal: ${at} console-retired ${line.id} after ${line.count} — did not recur after reload`;
|
|
653
684
|
case 'console-ack':
|
|
654
685
|
return `journal: ${at} console-ack ${line.id} (×${line.count}) by ${line.by}: ${line.reason}`;
|
|
686
|
+
case 'console-resolved':
|
|
687
|
+
return `journal: ${at} console-resolved ${line.id} after ${line.count} — ${line.by} cleared the condition it raised`;
|
|
655
688
|
}
|
|
656
689
|
}
|
|
657
690
|
|
|
@@ -68,7 +68,9 @@ export interface ProjectToolLoadError {
|
|
|
68
68
|
* is a FACT about the project; a host that cannot ask has not learned it.
|
|
69
69
|
*/
|
|
70
70
|
export const NO_PROJECT_MODULE_HOST_MESSAGE =
|
|
71
|
-
'This editor host cannot
|
|
71
|
+
'This editor host cannot RUN registered project tools (package.json#vgai.tools): callables ' +
|
|
72
|
+
'execute on the Node side. Contribution panels still load from project source; to run the ' +
|
|
73
|
+
'tools themselves, open the project in the Vite-backed dev or packaged editor.';
|
|
72
74
|
|
|
73
75
|
/** The catalog row that states {@link NO_PROJECT_MODULE_HOST_MESSAGE}. */
|
|
74
76
|
export function noProjectModuleHostError(): ProjectToolLoadError {
|
package/src/registry.ts
CHANGED
|
@@ -139,7 +139,7 @@ export type ToolOutcome<TResult = unknown> =
|
|
|
139
139
|
* `Error`, or a non-Error throw — normalized into INTERNAL_ERROR. The
|
|
140
140
|
* raw exception/message is never used as the identifying `code`, but it
|
|
141
141
|
* IS carried in `message` as well as `data.message`: every projection
|
|
142
|
-
* (the CLI's `vgai
|
|
142
|
+
* (the CLI's `vgai tool`, the oclif commands, `vgai screenshot`'s module
|
|
143
143
|
* lane) shows `error.message` and only some of them dump `data`, so a
|
|
144
144
|
* `message` that said nothing but "threw an unstructured exception"
|
|
145
145
|
* hid the one sentence the caller needed ("No editor connected — open
|
package/src/types.ts
CHANGED
|
@@ -127,5 +127,15 @@ export interface ToolContext {
|
|
|
127
127
|
* game binds `game.instance(ctx.instance)` from it; omitted means the sole
|
|
128
128
|
* live instance. */
|
|
129
129
|
instance?: string;
|
|
130
|
+
/** The HOST's project-module loader (the editor server's Vite SSR loader,
|
|
131
|
+
* wrapped with dependency-change invalidation). A tool that imports the
|
|
132
|
+
* project's own source at run time — `project.bake.preview` importing
|
|
133
|
+
* `src/models/barrel.ts` — MUST load through this rather than a raw
|
|
134
|
+
* `import()`: Node's ESM cache never invalidates, so a raw import returned
|
|
135
|
+
* the FIRST version of a model for the life of the server and every later
|
|
136
|
+
* edit re-rendered byte-identical until `vgai restart` (measured on the
|
|
137
|
+
* blind modeling bench, 2026-09-05: three restarts in one barrel). Absent
|
|
138
|
+
* only when the tool runs outside an editor server. */
|
|
139
|
+
loadProjectModule?: (absolutePath: string) => Promise<unknown>;
|
|
130
140
|
[key: string]: unknown;
|
|
131
141
|
}
|