summer-engine 4.3.0 → 4.5.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -1
- package/.codex-plugin/plugin.json +5 -3
- package/.cursor-plugin/plugin.json +4 -2
- package/.factory-plugin/plugin.json +4 -2
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +87 -0
- package/README.md +67 -27
- package/commands/start.md +19 -15
- package/commands/summer.md +1 -0
- package/dist/cli/command-usage.d.ts +3 -1
- package/dist/cli/command-usage.js +10 -4
- package/dist/cli/commands/create.d.ts +1 -25
- package/dist/cli/commands/create.js +10 -138
- package/dist/cli/commands/dev.d.ts +66 -0
- package/dist/cli/commands/dev.js +234 -0
- package/dist/cli/commands/install.d.ts +15 -58
- package/dist/cli/commands/install.js +31 -481
- package/dist/cli/commands/login.js +1 -1
- package/dist/cli/commands/ops.d.ts +17 -1
- package/dist/cli/commands/ops.js +97 -9
- package/dist/cli/commands/preview.d.ts +58 -0
- package/dist/cli/commands/preview.js +181 -0
- package/dist/cli/commands/run.d.ts +1 -17
- package/dist/cli/commands/run.js +17 -206
- package/dist/cli/index.js +5 -0
- package/dist/core/agent-host.d.ts +3 -0
- package/dist/core/agent-host.js +1 -1
- package/dist/core/api-client.d.ts +10 -2
- package/dist/core/api-client.js +13 -8
- package/dist/core/capabilities/agent-playbook.js +1 -0
- package/dist/core/capabilities/apps.d.ts +9 -9
- package/dist/core/capabilities/asset-import.d.ts +4 -1
- package/dist/core/capabilities/asset-import.js +15 -6
- package/dist/core/capabilities/capture-gameplay.js +1 -1
- package/dist/core/capabilities/connect-signal.d.ts +2 -2
- package/dist/core/capabilities/content-make/index.d.ts +81 -0
- package/dist/core/capabilities/content-make/index.js +706 -0
- package/dist/core/capabilities/content-make/media.d.ts +57 -0
- package/dist/core/capabilities/content-make/media.js +172 -0
- package/dist/core/capabilities/content-make/plan.d.ts +130 -0
- package/dist/core/capabilities/content-make/plan.js +433 -0
- package/dist/core/capabilities/content-make/render.d.ts +64 -0
- package/dist/core/capabilities/content-make/render.js +370 -0
- package/dist/core/capabilities/content-make/study.d.ts +13 -0
- package/dist/core/capabilities/content-make/study.js +165 -0
- package/dist/core/capabilities/content-make/types.d.ts +173 -0
- package/dist/core/capabilities/content-make/types.js +6 -0
- package/dist/core/capabilities/content-make/voice.d.ts +7 -0
- package/dist/core/capabilities/content-make/voice.js +27 -0
- package/dist/core/capabilities/events.d.ts +2 -2
- package/dist/core/capabilities/export-game.d.ts +4 -1
- package/dist/core/capabilities/export-game.js +4 -3
- package/dist/core/capabilities/export-presets.d.ts +2 -1
- package/dist/core/capabilities/export-presets.js +2 -1
- package/dist/core/capabilities/go-live.d.ts +14 -0
- package/dist/core/capabilities/go-live.js +408 -0
- package/dist/core/capabilities/image-generation.d.ts +6 -6
- package/dist/core/capabilities/item-manifest.d.ts +92 -0
- package/dist/core/capabilities/item-manifest.js +247 -0
- package/dist/core/capabilities/placement.d.ts +7 -7
- package/dist/core/capabilities/projects-local.d.ts +68 -0
- package/dist/core/capabilities/projects-local.js +172 -0
- package/dist/core/capabilities/publish-build.d.ts +13 -2
- package/dist/core/capabilities/publish-build.js +87 -21
- package/dist/core/capabilities/publish-download.js +1 -1
- package/dist/core/capabilities/runtime-control.d.ts +7 -2
- package/dist/core/capabilities/runtime-control.js +27 -1
- package/dist/core/capabilities/seeing/args.d.ts +2 -2
- package/dist/core/capabilities/store-checks.d.ts +45 -0
- package/dist/core/capabilities/store-checks.js +85 -0
- package/dist/core/capabilities/store-details.d.ts +37 -0
- package/dist/core/capabilities/store-details.js +94 -0
- package/dist/core/capabilities/summer-bundle.d.ts +21 -2
- package/dist/core/capabilities/summer-bundle.js +54 -7
- package/dist/core/capabilities/sync-items.d.ts +89 -0
- package/dist/core/capabilities/sync-items.js +352 -0
- package/dist/core/capabilities/tool-dispatch.js +66 -6
- package/dist/core/capabilities/video-ogv.d.ts +49 -0
- package/dist/core/capabilities/video-ogv.js +260 -0
- package/dist/core/capability-skew.js +2 -2
- package/dist/core/cli-usage-beacon.js +26 -2
- package/dist/core/cli-usage.d.ts +5 -2
- package/dist/core/cli-usage.js +8 -2
- package/dist/core/engine-install.d.ts +53 -0
- package/dist/core/engine-install.js +93 -0
- package/dist/core/engine-installer.d.ts +134 -0
- package/dist/core/engine-installer.js +760 -0
- package/dist/core/engine-launch.d.ts +126 -0
- package/dist/core/engine-launch.js +398 -0
- package/dist/core/engine-self-start.d.ts +63 -0
- package/dist/core/engine-self-start.js +282 -0
- package/dist/core/engine.d.ts +7 -1
- package/dist/core/engine.js +9 -2
- package/dist/core/error-codes.d.ts +4 -1
- package/dist/core/error-codes.js +7 -3
- package/dist/core/library-read.d.ts +2 -2
- package/dist/core/library-search.d.ts +2 -2
- package/dist/core/local-project.d.ts +50 -0
- package/dist/core/local-project.js +234 -0
- package/dist/core/ops/areas.js +9 -3
- package/dist/core/ops/catalog.d.ts +9 -0
- package/dist/core/ops/catalog.js +56 -4
- package/dist/core/ops/engine-answer.d.ts +4 -1
- package/dist/core/ops/engine-answer.js +16 -3
- package/dist/core/ops/engine.js +4 -4
- package/dist/core/ops/save-files.d.ts +6 -1
- package/dist/core/ops/save-files.js +67 -50
- package/dist/core/ops/search.js +9 -2
- package/dist/core/preview/input.d.ts +35 -0
- package/dist/core/preview/input.js +105 -0
- package/dist/core/preview/launch-config.d.ts +52 -0
- package/dist/core/preview/launch-config.js +137 -0
- package/dist/core/preview/page.d.ts +22 -0
- package/dist/core/preview/page.js +232 -0
- package/dist/core/preview/preview-source.d.ts +179 -0
- package/dist/core/preview/preview-source.js +350 -0
- package/dist/core/preview/server.d.ts +26 -0
- package/dist/core/preview/server.js +218 -0
- package/dist/core/telemetry.d.ts +20 -16
- package/dist/core/telemetry.js +92 -16
- package/dist/core/templates.d.ts +20 -0
- package/dist/core/templates.js +300 -3
- package/dist/core/tool-profiles.d.ts +2 -2
- package/dist/core/tool-profiles.js +1 -0
- package/dist/core/usage-fields.d.ts +41 -0
- package/dist/core/usage-fields.js +150 -0
- package/dist/core/util/sleep.d.ts +3 -0
- package/dist/core/util/sleep.js +12 -0
- package/dist/mcp/ops-surface.js +28 -2
- package/dist/mcp/server.d.ts +14 -4
- package/dist/mcp/server.js +42 -8
- package/dist/mcp/tool-surface.d.ts +8 -0
- package/dist/mcp/tool-surface.js +19 -2
- package/dist/mcp/tools/creator-tools.js +92 -14
- package/dist/mcp/tools/debug-tools.js +2 -0
- package/dist/mcp/tools/local-project-tools.d.ts +6 -0
- package/dist/mcp/tools/local-project-tools.js +33 -0
- package/dist/mcp/tools/navigation-tools.js +2 -1
- package/dist/mcp/tools/project-tools.js +5 -8
- package/dist/mcp/tools/seeing-tools.js +1 -1
- package/dist/mcp/tools/visual-tools.js +1 -1
- package/dist/mcp/tools/with-engine.js +1 -1
- package/gemini-extension.json +3 -1
- package/library/references/mcp-tools-reference/mcp-tools-reference.md +18 -5
- package/library/skills/README.md +12 -8
- package/library/skills/animated-loop/SKILL.md +10 -16
- package/library/skills/cinematic-cutscene/SKILL.md +7 -18
- package/library/skills/headless-scripting/SKILL.md +7 -4
- package/library/skills/in-game-purchases/SKILL.md +25 -8
- package/library/skills/in-game-purchases/resource.yaml +2 -1
- package/library/skills/launch-ready/SKILL.md +351 -0
- package/library/skills/launch-ready/resource.yaml +45 -0
- package/library/skills/make-content/SKILL.md +202 -0
- package/library/skills/make-content/resource.yaml +32 -0
- package/library/skills/make-game/SKILL.md +2 -0
- package/library/skills/make-game/resource.yaml +2 -1
- package/library/skills/navigate-summer/SKILL.md +4 -2
- package/library/skills/new-project/SKILL.md +15 -22
- package/library/skills/new-project/resource.yaml +2 -1
- package/library/skills/painted-25d-world/SKILL.md +366 -0
- package/library/skills/painted-25d-world/_building-blocks/blender-route.md +33 -0
- package/library/skills/painted-25d-world/_building-blocks/blockout_builder.gd +149 -0
- package/library/skills/painted-25d-world/_building-blocks/capture_paint_view.gd +32 -0
- package/library/skills/painted-25d-world/_building-blocks/paint_projection.gdshader +23 -0
- package/library/skills/painted-25d-world/_building-blocks/paintcam_check.gd +25 -0
- package/library/skills/painted-25d-world/_building-blocks/painted_door.gd +17 -0
- package/library/skills/painted-25d-world/_building-blocks/painted_location.gd +56 -0
- package/library/skills/painted-25d-world/_building-blocks/painted_player.gd +125 -0
- package/library/skills/painted-25d-world/_building-blocks/painted_world.gd +111 -0
- package/library/skills/painted-25d-world/_building-blocks/tools/cam.py +57 -0
- package/library/skills/painted-25d-world/_building-blocks/tools/detail4k.py +81 -0
- package/library/skills/painted-25d-world/_building-blocks/tools/overlay.py +17 -0
- package/library/skills/painted-25d-world/_building-blocks/tools/preview.py +60 -0
- package/library/skills/painted-25d-world/resource.yaml +39 -0
- package/library/skills/publish-your-game/SKILL.md +13 -14
- package/library/skills/publish-your-game/resource.yaml +4 -3
- package/library/skills/release-checklist/SKILL.md +188 -0
- package/library/skills/release-checklist/resource.yaml +40 -0
- package/library/skills/release-readiness/SKILL.md +1 -1
- package/library/skills/release-readiness/resource.yaml +1 -1
- package/library/skills/store-art/SKILL.md +1 -1
- package/library/skills/store-art/resource.yaml +1 -1
- package/library/skills/store-listing/SKILL.md +1 -1
- package/library/skills/store-listing/resource.yaml +1 -1
- package/library/skills/summer-cli/SKILL.md +2 -0
- package/library/skills/summer-store/SKILL.md +295 -39
- package/library/skills/summer-store/resource.yaml +9 -3
- package/library/skills/trailer-shot/SKILL.md +3 -3
- package/library/templates/README.md +5 -0
- package/library/tools/content-make/resource.yaml +132 -0
- package/library/tools/frame-shot/GUIDE.md +1 -1
- package/library/tools/go-live/resource.yaml +67 -0
- package/library/tools/import-from-url/GUIDE.md +1 -0
- package/library/tools/import-from-url-batch/GUIDE.md +1 -1
- package/library/tools/play/GUIDE.md +2 -0
- package/library/tools/projects-create-local/resource.yaml +55 -0
- package/library/tools/projects-open-local/resource.yaml +51 -0
- package/library/tools/publish-build/resource.yaml +3 -3
- package/library/tools/screenshot/GUIDE.md +1 -1
- package/library/tools/store-set-details/resource.yaml +114 -0
- package/library/tools/sync-items/resource.yaml +72 -0
- package/package.json +1 -1
- package/registry/generated/aliases.json +6 -0
- package/registry/generated/counts.json +3 -3
- package/registry/generated/gemini-extension.json +3 -1
- package/registry/generated/index.json +463 -37
- package/registry/generated/marketplace.claude.json +1 -1
- package/registry/generated/plugin.claude-directory.json +4 -2
- package/registry/generated/plugin.claude.json +3 -1
- package/registry/generated/plugin.codex.json +5 -3
- package/registry/generated/plugin.cursor.json +4 -2
- package/registry/generated/plugin.factory.json +4 -2
- package/registry/generated/server.json +2 -2
- package/registry/generated/skills-index.md +12 -8
- package/registry/generated/skills-registry.json +62 -5
- package/registry/operations/legacy-names.json +40 -0
- package/registry/operations/public-catalog.json +10144 -387
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { missingEngineOpResult } from "../capability-skew.js";
|
|
2
|
+
import { engineNotRunningFromConnectError } from "../error-codes.js";
|
|
3
|
+
import { resolveCurrentScene, resolveProjectName } from "../capabilities/project-context.js";
|
|
4
|
+
import { extractOpError } from "../capabilities/engine-receipt.js";
|
|
5
|
+
import { buildGameInputOp } from "../capabilities/runtime-control.js";
|
|
6
|
+
import { asRecord, numberFrom, stringFrom } from "../util/json.js";
|
|
7
|
+
import { engineInputEvent } from "./input.js";
|
|
8
|
+
/** Fastest pace: 5 frames a second. */
|
|
9
|
+
export const MIN_FRAME_INTERVAL_MS = 200;
|
|
10
|
+
/** Slowest pace while a frame still comes back. */
|
|
11
|
+
export const MAX_FRAME_INTERVAL_MS = 30_000;
|
|
12
|
+
/** Capture may take at most 1/share of the time: the editor's viewport
|
|
13
|
+
* read is light; a game frame stalls the game while it is read. */
|
|
14
|
+
export const LATENCY_SHARE = { editor: 2, game: 16 };
|
|
15
|
+
/** Editor pace once the view has stopped changing (2 fps). */
|
|
16
|
+
export const EDITOR_IDLE_INTERVAL_MS = 500;
|
|
17
|
+
/** Identical frames in a row before the editor counts as idle. */
|
|
18
|
+
export const IDLE_AFTER_SAME_FRAMES = 2;
|
|
19
|
+
/** Fresh frames on demand: at most one capture start this often. */
|
|
20
|
+
export const FRESH_MIN_GAP_MS = 2000;
|
|
21
|
+
/** Pace of the page while no editor answers. */
|
|
22
|
+
export const NO_EDITOR_INTERVAL_MS = 2000;
|
|
23
|
+
export const PLAY_CHECK_MS = 1000;
|
|
24
|
+
export const META_CHECK_MS = 5000;
|
|
25
|
+
export const RECONNECT_MS = 2000;
|
|
26
|
+
export const LINK_CHECK_MS = 2000;
|
|
27
|
+
/** After a running game's frame failed for a reason other than "not running",
|
|
28
|
+
* show the editor and try the game again after this long. */
|
|
29
|
+
export const GAME_RETRY_MS = 10_000;
|
|
30
|
+
/** A request that comes sooner than this share of the pace after the last
|
|
31
|
+
* capture started gets that capture's frame again: two tabs, or the page and
|
|
32
|
+
* an agent's own request, never double the engine's work. */
|
|
33
|
+
export const SHARE_FRAME_FRACTION = 0.75;
|
|
34
|
+
export const NO_EDITOR_MESSAGE = "Editor not running: run npx -y summer-engine@latest dev";
|
|
35
|
+
export const GAME_NOT_RUNNING_MESSAGE = "Game not running: showing the editor";
|
|
36
|
+
const EWMA_WEIGHT = 0.3;
|
|
37
|
+
function errorMessage(err) {
|
|
38
|
+
return err instanceof Error ? err.message : "unknown error";
|
|
39
|
+
}
|
|
40
|
+
function firstResult(value) {
|
|
41
|
+
const results = asRecord(value)?.results;
|
|
42
|
+
return Array.isArray(results) ? asRecord(results[0]) : null;
|
|
43
|
+
}
|
|
44
|
+
/** The pace for a `source` capture that takes `latencyMs`; `idle` = the
|
|
45
|
+
* editor view has stopped changing. */
|
|
46
|
+
export function frameIntervalMs(latencyMs, source = "editor", idle = false) {
|
|
47
|
+
const floor = source === "editor" && idle ? EDITOR_IDLE_INTERVAL_MS : MIN_FRAME_INTERVAL_MS;
|
|
48
|
+
if (latencyMs === null)
|
|
49
|
+
return floor;
|
|
50
|
+
const paced = Math.round(latencyMs * LATENCY_SHARE[source]);
|
|
51
|
+
return Math.min(MAX_FRAME_INTERVAL_MS, Math.max(floor, paced));
|
|
52
|
+
}
|
|
53
|
+
/** Schedule a batch frame by frame: a key pressed and released (or released
|
|
54
|
+
* and pressed again) in the same batch gets the second event one frame later,
|
|
55
|
+
* so a fast tap still reads as a press in the game. */
|
|
56
|
+
export function scheduleInputBatch(events) {
|
|
57
|
+
let frame = 0;
|
|
58
|
+
const lastFrame = new Map();
|
|
59
|
+
return events.map((event) => {
|
|
60
|
+
const key = event.type === "key" && typeof event.keycode === "number" ? event.keycode : null;
|
|
61
|
+
if (key !== null && lastFrame.get(key) === frame)
|
|
62
|
+
frame += 1;
|
|
63
|
+
if (key !== null)
|
|
64
|
+
lastFrame.set(key, frame);
|
|
65
|
+
return { ...event, at_frame: frame };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export class PreviewSource {
|
|
69
|
+
connectEngine;
|
|
70
|
+
now;
|
|
71
|
+
log;
|
|
72
|
+
client = null;
|
|
73
|
+
attempted = false;
|
|
74
|
+
lastConnectAt = -Infinity;
|
|
75
|
+
lastLinkCheckAt = -Infinity;
|
|
76
|
+
linkError = null;
|
|
77
|
+
playing = false;
|
|
78
|
+
playingScene = null;
|
|
79
|
+
lastPlayCheckAt = -Infinity;
|
|
80
|
+
project = null;
|
|
81
|
+
scene = null;
|
|
82
|
+
lastMetaAt = -Infinity;
|
|
83
|
+
gameBlockedUntil = -Infinity;
|
|
84
|
+
gameNote = null;
|
|
85
|
+
inputSupported = false;
|
|
86
|
+
latency = { game: null, editor: null };
|
|
87
|
+
current = null;
|
|
88
|
+
seq = 0;
|
|
89
|
+
inflight = null;
|
|
90
|
+
lastCaptureAt = -Infinity;
|
|
91
|
+
sameFrames = 0;
|
|
92
|
+
inputChain = Promise.resolve();
|
|
93
|
+
inputQueue = [];
|
|
94
|
+
constructor(options) {
|
|
95
|
+
this.connectEngine = options.connect;
|
|
96
|
+
this.now = options.now ?? (() => Date.now());
|
|
97
|
+
this.log = options.log ?? (() => undefined);
|
|
98
|
+
}
|
|
99
|
+
/** The current state, without asking the engine anything. */
|
|
100
|
+
status() {
|
|
101
|
+
const state = this.state();
|
|
102
|
+
const frame = state === "no_editor" ? null : this.current;
|
|
103
|
+
const latency = frame ? this.latency[frame.source] : null;
|
|
104
|
+
return {
|
|
105
|
+
service: "summer-preview",
|
|
106
|
+
state,
|
|
107
|
+
message: this.message(state),
|
|
108
|
+
project: this.project,
|
|
109
|
+
scene: state === "game" ? this.playingScene ?? this.scene : this.scene,
|
|
110
|
+
engineVersion: this.client?.getEngineVersion?.() ?? null,
|
|
111
|
+
frame: frame
|
|
112
|
+
? {
|
|
113
|
+
seq: frame.seq,
|
|
114
|
+
source: frame.source,
|
|
115
|
+
width: frame.width,
|
|
116
|
+
height: frame.height,
|
|
117
|
+
capturedAt: frame.capturedAt,
|
|
118
|
+
latencyMs: frame.latencyMs,
|
|
119
|
+
}
|
|
120
|
+
: null,
|
|
121
|
+
latencyMs: latency === null ? null : Math.round(latency),
|
|
122
|
+
intervalMs: state === "no_editor" || state === "connecting" ? NO_EDITOR_INTERVAL_MS : this.pace(),
|
|
123
|
+
input: this.client !== null && this.playing && this.inputSupported,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** The newest frame: captured now, or the last one when another viewer
|
|
127
|
+
* asked moments ago. `fresh` captures now unless a capture started in the
|
|
128
|
+
* last FRESH_MIN_GAP_MS. Null while no editor answers. One capture at a time. */
|
|
129
|
+
async frame(options = {}) {
|
|
130
|
+
if (this.inflight)
|
|
131
|
+
return this.inflight;
|
|
132
|
+
const reuseWithinMs = options.fresh ? FRESH_MIN_GAP_MS : this.pace() * SHARE_FRAME_FRACTION;
|
|
133
|
+
if (this.current && this.client && this.now() - this.lastCaptureAt < reuseWithinMs)
|
|
134
|
+
return this.current;
|
|
135
|
+
this.lastCaptureAt = this.now();
|
|
136
|
+
this.inflight = this.capture().finally(() => {
|
|
137
|
+
this.inflight = null;
|
|
138
|
+
});
|
|
139
|
+
return this.inflight;
|
|
140
|
+
}
|
|
141
|
+
/** Send clicks and keys to the running game, also while its frames cannot
|
|
142
|
+
* be read (a key release must never be lost). Calls are sent one at a time,
|
|
143
|
+
* in order; input that arrives meanwhile goes out as one script. */
|
|
144
|
+
async sendInput(inputs) {
|
|
145
|
+
if (!this.client || !this.playing) {
|
|
146
|
+
return { ok: false, error: "No game is running; clicks and keys go only to a running game." };
|
|
147
|
+
}
|
|
148
|
+
if (!this.inputSupported) {
|
|
149
|
+
return { ok: false, error: "This Summer Engine build cannot take input from the preview (it predates SimulateInputScript)." };
|
|
150
|
+
}
|
|
151
|
+
const geometry = this.current?.source === "game" ? this.current : null;
|
|
152
|
+
const events = inputs
|
|
153
|
+
.map((input) => engineInputEvent(input, geometry))
|
|
154
|
+
.filter((event) => event !== null);
|
|
155
|
+
if (events.length === 0)
|
|
156
|
+
return { ok: false, error: "Nothing to send: the key is not mapped, or the click is outside the frame." };
|
|
157
|
+
this.inputQueue.push(...events);
|
|
158
|
+
const client = this.client;
|
|
159
|
+
const run = this.inputChain.then(async () => {
|
|
160
|
+
const batch = this.inputQueue.splice(0);
|
|
161
|
+
if (batch.length === 0)
|
|
162
|
+
return { ok: true, sent: 0 };
|
|
163
|
+
const built = buildGameInputOp({ action: "script", events: scheduleInputBatch(batch), wait: false });
|
|
164
|
+
try {
|
|
165
|
+
const result = await client.executeOps([built.op], undefined, built.timeoutMs);
|
|
166
|
+
const failure = extractOpError(result);
|
|
167
|
+
return failure ? { ok: false, error: failure } : { ok: true, sent: batch.length };
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
this.log(`summer preview: input not sent: ${errorMessage(err)}`);
|
|
171
|
+
return { ok: false, error: "The engine did not answer; check that the game still runs." };
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
this.inputChain = run.catch(() => undefined);
|
|
175
|
+
return run;
|
|
176
|
+
}
|
|
177
|
+
pace() {
|
|
178
|
+
const frame = this.current;
|
|
179
|
+
if (!frame)
|
|
180
|
+
return MIN_FRAME_INTERVAL_MS;
|
|
181
|
+
return frameIntervalMs(this.latency[frame.source], frame.source, this.sameFrames >= IDLE_AFTER_SAME_FRAMES);
|
|
182
|
+
}
|
|
183
|
+
state() {
|
|
184
|
+
if (!this.client)
|
|
185
|
+
return this.attempted ? "no_editor" : "connecting";
|
|
186
|
+
if (this.current)
|
|
187
|
+
return this.current.source === "game" ? "game" : "editor";
|
|
188
|
+
return this.playing ? "game" : "editor";
|
|
189
|
+
}
|
|
190
|
+
message(state) {
|
|
191
|
+
switch (state) {
|
|
192
|
+
case "connecting":
|
|
193
|
+
return "Looking for Summer Engine";
|
|
194
|
+
case "no_editor":
|
|
195
|
+
return this.linkError ?? NO_EDITOR_MESSAGE;
|
|
196
|
+
case "game":
|
|
197
|
+
return `Game running${this.playingScene ? `: ${this.playingScene}` : ""}`;
|
|
198
|
+
case "editor":
|
|
199
|
+
if (this.playing && this.gameNote)
|
|
200
|
+
return `Game running, but its frame could not be read (${this.gameNote}): showing the editor`;
|
|
201
|
+
return GAME_NOT_RUNNING_MESSAGE;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
drop() {
|
|
205
|
+
this.client = null;
|
|
206
|
+
this.current = null;
|
|
207
|
+
this.playing = false;
|
|
208
|
+
this.playingScene = null;
|
|
209
|
+
this.inputSupported = false;
|
|
210
|
+
this.lastConnectAt = -Infinity;
|
|
211
|
+
}
|
|
212
|
+
/** The connected client, connecting or following a restart as needed. */
|
|
213
|
+
async engine() {
|
|
214
|
+
const now = this.now();
|
|
215
|
+
if (this.client && now - this.lastLinkCheckAt >= LINK_CHECK_MS) {
|
|
216
|
+
this.lastLinkCheckAt = now;
|
|
217
|
+
if (await this.client.credentialsChanged().catch(() => false))
|
|
218
|
+
this.drop();
|
|
219
|
+
}
|
|
220
|
+
if (this.client)
|
|
221
|
+
return this.client;
|
|
222
|
+
if (now - this.lastConnectAt < RECONNECT_MS)
|
|
223
|
+
return null;
|
|
224
|
+
this.lastConnectAt = now;
|
|
225
|
+
this.attempted = true;
|
|
226
|
+
try {
|
|
227
|
+
const client = await this.connectEngine();
|
|
228
|
+
this.client = client;
|
|
229
|
+
this.linkError = null;
|
|
230
|
+
this.lastLinkCheckAt = now;
|
|
231
|
+
this.lastPlayCheckAt = -Infinity;
|
|
232
|
+
this.lastMetaAt = -Infinity;
|
|
233
|
+
this.gameBlockedUntil = -Infinity;
|
|
234
|
+
this.gameNote = null;
|
|
235
|
+
this.inputSupported = !missingEngineOpResult(client, "SimulateInputScript", "");
|
|
236
|
+
return client;
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
const reason = errorMessage(err);
|
|
240
|
+
const notRunning = engineNotRunningFromConnectError(reason);
|
|
241
|
+
this.linkError = notRunning
|
|
242
|
+
? /^No running Summer editor matches/.test(reason)
|
|
243
|
+
? `Editor not running for this project: run npx -y summer-engine@latest dev`
|
|
244
|
+
: null
|
|
245
|
+
: `Summer Engine: ${reason.split("\n")[0]}`;
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/** Whether a game runs (and which scene). False when the editor is gone. */
|
|
250
|
+
async refreshPlaying(client) {
|
|
251
|
+
const now = this.now();
|
|
252
|
+
if (now - this.lastPlayCheckAt < PLAY_CHECK_MS)
|
|
253
|
+
return true;
|
|
254
|
+
this.lastPlayCheckAt = now;
|
|
255
|
+
try {
|
|
256
|
+
const result = firstResult(await client.executeOps([{ op: "IsGameRunning" }]));
|
|
257
|
+
const playing = result?.playing === true || result?.running === true;
|
|
258
|
+
if (playing && !this.playing)
|
|
259
|
+
this.gameBlockedUntil = -Infinity;
|
|
260
|
+
this.playing = playing;
|
|
261
|
+
this.playingScene = playing ? stringFrom(result?.scene) || null : null;
|
|
262
|
+
if (!playing)
|
|
263
|
+
this.gameNote = null;
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// The editor stopped answering: look for it again.
|
|
268
|
+
this.drop();
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async refreshMeta(client) {
|
|
273
|
+
const now = this.now();
|
|
274
|
+
if (now - this.lastMetaAt < META_CHECK_MS)
|
|
275
|
+
return;
|
|
276
|
+
this.lastMetaAt = now;
|
|
277
|
+
const [projectState, sceneState] = await Promise.all([
|
|
278
|
+
client.getProjectState().catch(() => null),
|
|
279
|
+
client.getSceneState().catch(() => null),
|
|
280
|
+
]);
|
|
281
|
+
if (projectState)
|
|
282
|
+
this.project = resolveProjectName(projectState, null);
|
|
283
|
+
if (sceneState || projectState)
|
|
284
|
+
this.scene = resolveCurrentScene(projectState, sceneState, null);
|
|
285
|
+
}
|
|
286
|
+
async capture() {
|
|
287
|
+
const client = await this.engine();
|
|
288
|
+
if (!client)
|
|
289
|
+
return null;
|
|
290
|
+
if (!(await this.refreshPlaying(client)))
|
|
291
|
+
return null;
|
|
292
|
+
await this.refreshMeta(client);
|
|
293
|
+
if (this.playing && this.now() >= this.gameBlockedUntil) {
|
|
294
|
+
const frame = await this.read(client, "game");
|
|
295
|
+
if (frame)
|
|
296
|
+
return frame;
|
|
297
|
+
}
|
|
298
|
+
const frame = await this.read(client, "editor");
|
|
299
|
+
if (frame)
|
|
300
|
+
return frame;
|
|
301
|
+
// Keep showing the last frame; the next status check finds a lost editor.
|
|
302
|
+
this.lastPlayCheckAt = -Infinity;
|
|
303
|
+
return this.current;
|
|
304
|
+
}
|
|
305
|
+
async read(client, source) {
|
|
306
|
+
const started = this.now();
|
|
307
|
+
let snap;
|
|
308
|
+
try {
|
|
309
|
+
snap = source === "game" ? await client.gameSnapshot({ saveFile: false }) : await client.viewportSnapshot({ saveFile: false });
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
snap = { ok: false, error: errorMessage(err) };
|
|
313
|
+
}
|
|
314
|
+
const latencyMs = this.now() - started;
|
|
315
|
+
if (!snap.ok || !snap.base64) {
|
|
316
|
+
if (source === "game") {
|
|
317
|
+
if (snap.failureReason === "not_running") {
|
|
318
|
+
this.playing = false;
|
|
319
|
+
this.playingScene = null;
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
this.gameNote = snap.failureReason ?? snap.error ?? "no image";
|
|
323
|
+
this.gameBlockedUntil = this.now() + GAME_RETRY_MS;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
const previous = this.latency[source];
|
|
329
|
+
this.latency[source] = previous === null ? latencyMs : previous + EWMA_WEIGHT * (latencyMs - previous);
|
|
330
|
+
if (source === "game")
|
|
331
|
+
this.gameNote = null;
|
|
332
|
+
const bytes = Buffer.from(snap.base64, "base64");
|
|
333
|
+
const same = this.current?.source === source && this.current.bytes.equals(bytes);
|
|
334
|
+
this.sameFrames = same ? this.sameFrames + 1 : 0;
|
|
335
|
+
this.seq += 1;
|
|
336
|
+
this.current = {
|
|
337
|
+
seq: this.seq,
|
|
338
|
+
source,
|
|
339
|
+
bytes,
|
|
340
|
+
mime: snap.mime ?? "image/png",
|
|
341
|
+
width: snap.width ?? 0,
|
|
342
|
+
height: snap.height ?? 0,
|
|
343
|
+
sourceWidth: numberFrom(snap.metadata?.source_width),
|
|
344
|
+
sourceHeight: numberFrom(snap.metadata?.source_height),
|
|
345
|
+
capturedAt: this.now(),
|
|
346
|
+
latencyMs,
|
|
347
|
+
};
|
|
348
|
+
return this.current;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PreviewSource, type PreviewEngine } from "./preview-source.js";
|
|
2
|
+
export declare const PREVIEW_HOST = "127.0.0.1";
|
|
3
|
+
/** Ports tried after a taken default port. */
|
|
4
|
+
export declare const PORT_SEARCH_SPAN = 20;
|
|
5
|
+
export interface PreviewServerOptions {
|
|
6
|
+
/** First port to try. */
|
|
7
|
+
port: number;
|
|
8
|
+
/** true: that port or an error (a host that launched us expects it).
|
|
9
|
+
* false: the next free port up to PORT_SEARCH_SPAN above it. 0 = any. */
|
|
10
|
+
strictPort: boolean;
|
|
11
|
+
connect: () => Promise<PreviewEngine>;
|
|
12
|
+
now?: () => number;
|
|
13
|
+
/** Where failure details go (default stderr); the page gets a short fixed text. */
|
|
14
|
+
log?: (line: string) => void;
|
|
15
|
+
}
|
|
16
|
+
export interface PreviewServer {
|
|
17
|
+
url: string;
|
|
18
|
+
port: number;
|
|
19
|
+
source: PreviewSource;
|
|
20
|
+
close(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export declare class PortInUseError extends Error {
|
|
23
|
+
readonly port: number;
|
|
24
|
+
constructor(port: number);
|
|
25
|
+
}
|
|
26
|
+
export declare function startPreviewServer(options: PreviewServerOptions): Promise<PreviewServer>;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `summer preview` HTTP server: loopback only, one page and three routes.
|
|
3
|
+
*
|
|
4
|
+
* GET / the page (./page.ts)
|
|
5
|
+
* GET /api/frame the newest frame (image bytes, status in x-summer-status),
|
|
6
|
+
* or 503 + the status as JSON while no editor answers;
|
|
7
|
+
* ?fresh=1 captures now (a check that needs the game as it is)
|
|
8
|
+
* GET /api/status the status as JSON, without capturing
|
|
9
|
+
* POST /api/input {inputs:[{kind:"key",code,pressed} | {kind:"click",x,y,button}]}
|
|
10
|
+
* to the running game
|
|
11
|
+
*
|
|
12
|
+
* Listens on 127.0.0.1 and, where the machine has it, on ::1 at the same port,
|
|
13
|
+
* so http://localhost:<port> (what Claude Code Desktop's Browser pane opens)
|
|
14
|
+
* reaches it either way. Requests must name a loopback Host (stops a web page
|
|
15
|
+
* from reaching it through DNS rebinding); the API refuses requests a browser
|
|
16
|
+
* marks as coming from another site (Sec-Fetch-Site), so another page cannot
|
|
17
|
+
* make it capture with an <img>; input must be a same-origin JSON POST.
|
|
18
|
+
*
|
|
19
|
+
* No frame-ancestors in the CSP: Desktop's docs do not say how the Browser
|
|
20
|
+
* pane hosts a page, and a frame-ancestors rule would blank it if the pane
|
|
21
|
+
* frames it. Instead the page itself sends no input while it is framed.
|
|
22
|
+
*/
|
|
23
|
+
import { createServer } from "node:http";
|
|
24
|
+
import { parsePreviewInputs } from "./input.js";
|
|
25
|
+
import { PREVIEW_PAGE_HTML } from "./page.js";
|
|
26
|
+
import { PreviewSource } from "./preview-source.js";
|
|
27
|
+
export const PREVIEW_HOST = "127.0.0.1";
|
|
28
|
+
/** Ports tried after a taken default port. */
|
|
29
|
+
export const PORT_SEARCH_SPAN = 20;
|
|
30
|
+
const CONTENT_SECURITY_POLICY = "default-src 'none'; img-src 'self' blob:; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; base-uri 'none'; form-action 'none'";
|
|
31
|
+
const MAX_INPUT_BODY_BYTES = 16 * 1024;
|
|
32
|
+
export class PortInUseError extends Error {
|
|
33
|
+
port;
|
|
34
|
+
constructor(port) {
|
|
35
|
+
super(`Port ${port} is in use.`);
|
|
36
|
+
this.port = port;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function sendJson(res, status, body) {
|
|
40
|
+
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
41
|
+
res.end(JSON.stringify(body));
|
|
42
|
+
}
|
|
43
|
+
function allowedHosts(port) {
|
|
44
|
+
return new Set([`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`]);
|
|
45
|
+
}
|
|
46
|
+
async function readBody(req) {
|
|
47
|
+
let size = 0;
|
|
48
|
+
const chunks = [];
|
|
49
|
+
for await (const chunk of req) {
|
|
50
|
+
size += chunk.length;
|
|
51
|
+
if (size > MAX_INPUT_BODY_BYTES)
|
|
52
|
+
return null;
|
|
53
|
+
chunks.push(chunk);
|
|
54
|
+
}
|
|
55
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
56
|
+
}
|
|
57
|
+
function handler(source, getPort) {
|
|
58
|
+
return async (req, res) => {
|
|
59
|
+
const port = getPort();
|
|
60
|
+
const hosts = allowedHosts(port);
|
|
61
|
+
if (!hosts.has(req.headers.host ?? "")) {
|
|
62
|
+
sendJson(res, 403, { ok: false, error: "Open the preview at http://127.0.0.1:" + port });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const [path, query = ""] = (req.url ?? "/").split("?");
|
|
66
|
+
const site = req.headers["sec-fetch-site"];
|
|
67
|
+
if (path.startsWith("/api/") && (site === "cross-site" || site === "same-site")) {
|
|
68
|
+
sendJson(res, 403, { ok: false, error: "The preview API answers only its own page." });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (req.method === "GET" && (path === "/" || path === "/index.html")) {
|
|
72
|
+
res.writeHead(200, {
|
|
73
|
+
"content-type": "text/html; charset=utf-8",
|
|
74
|
+
"cache-control": "no-store",
|
|
75
|
+
"content-security-policy": CONTENT_SECURITY_POLICY,
|
|
76
|
+
"x-content-type-options": "nosniff",
|
|
77
|
+
"referrer-policy": "no-referrer",
|
|
78
|
+
});
|
|
79
|
+
res.end(PREVIEW_PAGE_HTML);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (req.method === "GET" && path === "/api/status") {
|
|
83
|
+
sendJson(res, 200, source.status());
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (req.method === "GET" && path === "/api/frame") {
|
|
87
|
+
const frame = await source.frame({ fresh: new URLSearchParams(query).get("fresh") === "1" });
|
|
88
|
+
const status = source.status();
|
|
89
|
+
if (!frame || status.state === "no_editor") {
|
|
90
|
+
sendJson(res, 503, status);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
res.writeHead(200, {
|
|
94
|
+
"content-type": frame.mime,
|
|
95
|
+
"content-length": frame.bytes.length,
|
|
96
|
+
"cache-control": "no-store",
|
|
97
|
+
"x-content-type-options": "nosniff",
|
|
98
|
+
"x-summer-status": encodeURIComponent(JSON.stringify(status)),
|
|
99
|
+
});
|
|
100
|
+
res.end(frame.bytes);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (path === "/api/input") {
|
|
104
|
+
if (req.method !== "POST") {
|
|
105
|
+
sendJson(res, 405, { ok: false, error: "POST only" });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const origin = req.headers.origin;
|
|
109
|
+
const sameOrigin = !origin || [...hosts].some((host) => origin === `http://${host}`);
|
|
110
|
+
const json = (req.headers["content-type"] ?? "").split(";")[0].trim() === "application/json";
|
|
111
|
+
if (!sameOrigin || !json) {
|
|
112
|
+
sendJson(res, 403, { ok: false, error: "Input is taken only from the preview page (same origin, JSON)." });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const text = await readBody(req);
|
|
116
|
+
let body = null;
|
|
117
|
+
try {
|
|
118
|
+
body = text === null ? null : JSON.parse(text);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
body = null;
|
|
122
|
+
}
|
|
123
|
+
const inputs = parsePreviewInputs(body);
|
|
124
|
+
if (!inputs) {
|
|
125
|
+
sendJson(res, 400, { ok: false, error: "Expected {inputs:[{kind:'key',code,pressed} | {kind:'click',x,y,button}]} (1 to 64)." });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const result = await source.sendInput(inputs);
|
|
129
|
+
sendJson(res, result.ok ? 200 : 409, result);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
sendJson(res, 404, { ok: false, error: "Not found. The preview is at /" });
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function listen(server, port, host) {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const onError = (err) => {
|
|
138
|
+
server.off("listening", onListening);
|
|
139
|
+
reject(err);
|
|
140
|
+
};
|
|
141
|
+
const onListening = () => {
|
|
142
|
+
server.off("error", onError);
|
|
143
|
+
resolve();
|
|
144
|
+
};
|
|
145
|
+
server.once("error", onError);
|
|
146
|
+
server.once("listening", onListening);
|
|
147
|
+
server.listen(port, host);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function closeServer(server) {
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
server.close(() => resolve());
|
|
153
|
+
server.closeAllConnections?.();
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function safe(handle, log) {
|
|
157
|
+
return (req, res) => {
|
|
158
|
+
handle(req, res).catch((err) => {
|
|
159
|
+
// Details stay in the local log; the page gets a fixed answer.
|
|
160
|
+
log(`summer preview: ${req.method} ${(req.url ?? "/").split("?")[0]} failed: ${err instanceof Error ? err.message : "unknown error"}`);
|
|
161
|
+
if (res.headersSent)
|
|
162
|
+
res.end();
|
|
163
|
+
else
|
|
164
|
+
sendJson(res, 500, { ok: false, error: "internal_error" });
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** Bind 127.0.0.1 (required) and ::1 (when the machine has IPv6) on one port. */
|
|
169
|
+
async function bindPair(port, handle, log) {
|
|
170
|
+
const v4 = createServer(safe(handle, log));
|
|
171
|
+
await listen(v4, port, PREVIEW_HOST);
|
|
172
|
+
const bound = v4.address().port;
|
|
173
|
+
const v6 = createServer(safe(handle, log));
|
|
174
|
+
try {
|
|
175
|
+
await listen(v6, bound, "::1");
|
|
176
|
+
return [v4, v6];
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
const code = err.code;
|
|
180
|
+
if (code === "EADDRINUSE") {
|
|
181
|
+
// Something else answers localhost on this port over IPv6: the Browser
|
|
182
|
+
// pane could reach it instead of us. Treat the port as taken.
|
|
183
|
+
await closeServer(v4);
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
return [v4];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
export async function startPreviewServer(options) {
|
|
190
|
+
const log = options.log ?? ((line) => process.stderr.write(`${line}\n`));
|
|
191
|
+
const source = new PreviewSource({ connect: options.connect, now: options.now, log });
|
|
192
|
+
let port = options.port;
|
|
193
|
+
const handle = handler(source, () => port);
|
|
194
|
+
const last = options.strictPort || options.port === 0 ? options.port : options.port + PORT_SEARCH_SPAN;
|
|
195
|
+
let servers = null;
|
|
196
|
+
for (let candidate = options.port; candidate <= last; candidate += 1) {
|
|
197
|
+
try {
|
|
198
|
+
servers = await bindPair(candidate, handle, log);
|
|
199
|
+
port = servers[0].address().port;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
if (err.code !== "EADDRINUSE")
|
|
204
|
+
throw err;
|
|
205
|
+
if (candidate === last)
|
|
206
|
+
throw new PortInUseError(options.strictPort ? options.port : candidate);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const bound = servers;
|
|
210
|
+
return {
|
|
211
|
+
url: `http://${PREVIEW_HOST}:${port}`,
|
|
212
|
+
port,
|
|
213
|
+
source,
|
|
214
|
+
close: async () => {
|
|
215
|
+
await Promise.all(bound.map(closeServer));
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
package/dist/core/telemetry.d.ts
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Usage beacons for the local MCP server. Fire-and-forget, never blocks, never throws.
|
|
3
|
-
*
|
|
4
|
-
* - One session ping per MCP server lifetime (mcp_session_start), on the
|
|
5
|
-
* first tool call, with the host app's name from the MCP handshake.
|
|
6
|
-
* - One line per local tool call (mcp_tool_call): the tool name, the
|
|
7
|
-
* Summer operation it ran, success, duration, the host app's name from the
|
|
8
|
-
* MCP handshake, and the tool surface and profile. Nothing else: no
|
|
9
|
-
* arguments, no results, no project or file names.
|
|
10
|
-
*
|
|
11
|
-
* Both go to /api/mcp/log-local-call, only when signed in (the account token
|
|
12
|
-
* attributes them), and never when SUMMER_NO_TELEMETRY=1 or DO_NOT_TRACK=1.
|
|
13
|
-
* The endpoint always returns allowed:true and never gates; the response is
|
|
14
|
-
* ignored.
|
|
15
|
-
*/
|
|
16
1
|
import { isTelemetryDisabled } from "./agent-host.js";
|
|
2
|
+
import { type UsageFields } from "./usage-fields.js";
|
|
17
3
|
export { isTelemetryDisabled };
|
|
18
4
|
/**
|
|
19
5
|
* Ping the telemetry endpoint once per MCP server lifetime.
|
|
@@ -38,6 +24,8 @@ export interface LocalToolCall {
|
|
|
38
24
|
profile?: string;
|
|
39
25
|
/** A failed call's error code (toolErrorCode); never sent for a call that worked. */
|
|
40
26
|
error?: string | null;
|
|
27
|
+
/** What the call noted (usage-fields.ts noteToolUsage) and the bound project. */
|
|
28
|
+
usage?: UsageFields;
|
|
41
29
|
}
|
|
42
30
|
/**
|
|
43
31
|
* The code a failed tool result names, read from its first text block: the
|
|
@@ -48,7 +36,23 @@ export declare function toolErrorCode(result: unknown, fallback?: string): strin
|
|
|
48
36
|
/** Report one local tool call: tool name, success, duration, host app name. */
|
|
49
37
|
export declare function recordLocalToolCall(call: LocalToolCall): void;
|
|
50
38
|
/** POST one usage line with the account token; resolves when sent or given up. Never throws. */
|
|
51
|
-
export declare function postUsage(body: Record<string, unknown>, timeoutMs?: number
|
|
39
|
+
export declare function postUsage(body: Record<string, unknown>, timeoutMs?: number, options?: {
|
|
40
|
+
anonymous?: boolean;
|
|
41
|
+
}): Promise<void>;
|
|
42
|
+
/** The one-time notice, printed to stderr (never stdout: `summer mcp` speaks MCP there). */
|
|
43
|
+
export declare const TELEMETRY_NOTICE: string;
|
|
44
|
+
export interface FirstRunDeps {
|
|
45
|
+
env?: NodeJS.ProcessEnv;
|
|
46
|
+
write?: (text: string) => void;
|
|
47
|
+
send?: (body: Record<string, unknown>) => void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The first `summer` command on this machine: print the notice once, then
|
|
51
|
+
* send one anonymous cli_first_run line (install id, command name, agent
|
|
52
|
+
* app, operating system). Nothing when opted out, in CI, or when the marker
|
|
53
|
+
* cannot be written (no line without the notice). Returns whether it ran.
|
|
54
|
+
*/
|
|
55
|
+
export declare function noteFirstRun(command: string, deps?: FirstRunDeps): Promise<boolean>;
|
|
52
56
|
/**
|
|
53
57
|
* Reset the once-per-session guard. Test-only.
|
|
54
58
|
*/
|