@dcl-regenesislabs/opendcl 0.1.0-22234509684.commit-63dfd19
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/README.md +234 -0
- package/context/components-reference.md +113 -0
- package/context/open-source-3d-assets.md +705 -0
- package/context/sdk7-complete-reference.md +3684 -0
- package/context/sdk7-examples.md +1709 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/scene-context.d.ts +97 -0
- package/dist/scene-context.d.ts.map +1 -0
- package/dist/scene-context.js +203 -0
- package/dist/scene-context.js.map +1 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +4 -0
- package/dist/utils.js.map +1 -0
- package/extensions/dcl-context.ts +123 -0
- package/extensions/dcl-deploy.ts +89 -0
- package/extensions/dcl-header.ts +162 -0
- package/extensions/dcl-init.ts +62 -0
- package/extensions/dcl-preview.ts +144 -0
- package/extensions/dcl-setup-ollama.ts +312 -0
- package/extensions/dcl-setup.ts +96 -0
- package/extensions/dcl-status.ts +88 -0
- package/extensions/dcl-tasks.ts +102 -0
- package/extensions/dcl-update-check.ts +79 -0
- package/extensions/dcl-validate.ts +80 -0
- package/extensions/plan-mode/index.ts +340 -0
- package/extensions/plan-mode/utils.ts +168 -0
- package/extensions/process-registry.ts +25 -0
- package/extensions/scene-utils.ts +31 -0
- package/package.json +65 -0
- package/prompts/explain.md +16 -0
- package/prompts/review.md +19 -0
- package/prompts/system.md +126 -0
- package/skills/add-3d-models/SKILL.md +115 -0
- package/skills/add-interactivity/SKILL.md +176 -0
- package/skills/advanced-input/SKILL.md +238 -0
- package/skills/advanced-rendering/SKILL.md +235 -0
- package/skills/animations-tweens/SKILL.md +173 -0
- package/skills/audio-video/SKILL.md +167 -0
- package/skills/authoritative-server/SKILL.md +329 -0
- package/skills/build-ui/SKILL.md +231 -0
- package/skills/camera-control/SKILL.md +199 -0
- package/skills/create-scene/SKILL.md +67 -0
- package/skills/deploy-scene/SKILL.md +106 -0
- package/skills/deploy-worlds/SKILL.md +107 -0
- package/skills/lighting-environment/SKILL.md +216 -0
- package/skills/multiplayer-sync/SKILL.md +132 -0
- package/skills/nft-blockchain/SKILL.md +246 -0
- package/skills/optimize-scene/SKILL.md +160 -0
- package/skills/player-avatar/SKILL.md +239 -0
- package/skills/smart-items/SKILL.md +181 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DCL Setup Extension
|
|
3
|
+
*
|
|
4
|
+
* Registers the /setup command that walks users through
|
|
5
|
+
* configuring an LLM provider — either via subscription login
|
|
6
|
+
* (easiest) or by pasting an API key.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExtensionFactory } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
|
|
14
|
+
export const LOGIN_PROVIDERS = [
|
|
15
|
+
{ id: "anthropic", label: "Anthropic (Claude Pro/Max)" },
|
|
16
|
+
{ id: "openai-codex", label: "OpenAI (ChatGPT Plus/Pro)" },
|
|
17
|
+
{ id: "github-copilot", label: "GitHub Copilot" },
|
|
18
|
+
{ id: "google-gemini-cli", label: "Google (Gemini)" },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const API_KEY_PROVIDERS = [
|
|
22
|
+
{ id: "anthropic", label: "Anthropic (Claude)", envVar: "ANTHROPIC_API_KEY" },
|
|
23
|
+
{ id: "openai", label: "OpenAI (GPT)", envVar: "OPENAI_API_KEY" },
|
|
24
|
+
{ id: "google", label: "Google (Gemini)", envVar: "GEMINI_API_KEY" },
|
|
25
|
+
{ id: "groq", label: "Groq", envVar: "GROQ_API_KEY" },
|
|
26
|
+
{ id: "mistral", label: "Mistral", envVar: "MISTRAL_API_KEY" },
|
|
27
|
+
{ id: "xai", label: "xAI (Grok)", envVar: "XAI_API_KEY" },
|
|
28
|
+
{ id: "openrouter", label: "OpenRouter", envVar: "OPENROUTER_API_KEY" },
|
|
29
|
+
{ id: "cerebras", label: "Cerebras", envVar: "CEREBRAS_API_KEY" },
|
|
30
|
+
{ id: "huggingface", label: "Hugging Face", envVar: "HF_TOKEN" },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export async function readAuthConfig(authPath: string): Promise<Record<string, unknown>> {
|
|
34
|
+
try {
|
|
35
|
+
const content = await readFile(authPath, "utf-8");
|
|
36
|
+
return JSON.parse(content);
|
|
37
|
+
} catch {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function writeAuthKey(authPath: string, providerId: string, apiKey: string): Promise<void> {
|
|
43
|
+
const existing = await readAuthConfig(authPath);
|
|
44
|
+
existing[providerId] = { type: "api_key", key: apiKey };
|
|
45
|
+
await mkdir(dirname(authPath), { recursive: true });
|
|
46
|
+
await writeFile(authPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const LOGIN_SUFFIX = " (Login with subscription)";
|
|
50
|
+
const API_KEY_SUFFIX = " (API key)";
|
|
51
|
+
|
|
52
|
+
const extension: ExtensionFactory = (pi) => {
|
|
53
|
+
pi.registerCommand("setup", {
|
|
54
|
+
description: "Configure an LLM provider (login or API key)",
|
|
55
|
+
handler: async (_args, ctx) => {
|
|
56
|
+
const loginLabels = LOGIN_PROVIDERS.map((p) => p.label + LOGIN_SUFFIX);
|
|
57
|
+
const apiKeyLabels = API_KEY_PROVIDERS.map((p) => p.label + API_KEY_SUFFIX);
|
|
58
|
+
const allLabels = [...loginLabels, ...apiKeyLabels];
|
|
59
|
+
|
|
60
|
+
const selected = await ctx.ui.select("Choose a provider", allLabels);
|
|
61
|
+
if (!selected) {
|
|
62
|
+
ctx.ui.notify("Setup cancelled.", "info");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (selected.endsWith(LOGIN_SUFFIX)) {
|
|
67
|
+
ctx.ui.notify("Type /login and select your provider to authenticate.", "info");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const provider = API_KEY_PROVIDERS.find((p) => p.label + API_KEY_SUFFIX === selected);
|
|
72
|
+
if (!provider) {
|
|
73
|
+
ctx.ui.notify("Invalid selection.", "error");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const apiKey = await ctx.ui.input(`Paste your ${provider.label} API key`);
|
|
78
|
+
if (!apiKey) {
|
|
79
|
+
ctx.ui.notify("Setup cancelled.", "info");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Persist to auth.json for future sessions
|
|
84
|
+
const authPath = join(homedir(), ".opendcl", "agent", "auth.json");
|
|
85
|
+
await writeAuthKey(authPath, provider.id, apiKey);
|
|
86
|
+
|
|
87
|
+
// Register in-memory so models are available immediately
|
|
88
|
+
// (session.reload() does not re-read auth.json)
|
|
89
|
+
pi.registerProvider(provider.id, { apiKey });
|
|
90
|
+
|
|
91
|
+
ctx.ui.notify(`${provider.label} configured! Press Ctrl+P to select a model.`, "info");
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export default extension;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DCL Status Extension
|
|
3
|
+
*
|
|
4
|
+
* Shows elapsed time and output token count in the working message spinner
|
|
5
|
+
* during LLM inference, replacing the default "Working..." text with something
|
|
6
|
+
* like "Thinking... (23s · ↓ 1.2k tokens)".
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExtensionFactory } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
export function formatElapsed(ms: number): string {
|
|
12
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
13
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
14
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
15
|
+
const seconds = totalSeconds % 60;
|
|
16
|
+
return `${minutes}m ${seconds}s`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatTokens(count: number): string {
|
|
20
|
+
if (count < 1000) return `${count}`;
|
|
21
|
+
return `${(count / 1000).toFixed(1)}k`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type Phase = "Thinking" | "Generating" | "Tool call";
|
|
25
|
+
|
|
26
|
+
const extension: ExtensionFactory = (pi) => {
|
|
27
|
+
let startTime = 0;
|
|
28
|
+
let outputTokens = 0;
|
|
29
|
+
let phase: Phase = "Thinking";
|
|
30
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
31
|
+
|
|
32
|
+
function update(ctx: { ui: { setWorkingMessage(msg?: string): void } }) {
|
|
33
|
+
const elapsed = formatElapsed(Date.now() - startTime);
|
|
34
|
+
const tokens = formatTokens(outputTokens);
|
|
35
|
+
ctx.ui.setWorkingMessage(`${phase}... (${elapsed} · ↓ ${tokens} tokens)`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function cleanup(ctx: { ui: { setWorkingMessage(msg?: string): void } }) {
|
|
39
|
+
if (timer) {
|
|
40
|
+
clearInterval(timer);
|
|
41
|
+
timer = null;
|
|
42
|
+
}
|
|
43
|
+
ctx.ui.setWorkingMessage();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pi.on("turn_start", async (event, ctx) => {
|
|
47
|
+
startTime = (event as { timestamp?: number }).timestamp ?? Date.now();
|
|
48
|
+
outputTokens = 0;
|
|
49
|
+
phase = "Thinking";
|
|
50
|
+
cleanup(ctx);
|
|
51
|
+
update(ctx);
|
|
52
|
+
timer = setInterval(() => update(ctx), 1000);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
pi.on("message_update", async (event, ctx) => {
|
|
56
|
+
const e = event as {
|
|
57
|
+
message?: { usage?: { output?: number } };
|
|
58
|
+
assistantMessageEvent?: { type?: string };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (e.message?.usage?.output != null) {
|
|
62
|
+
outputTokens = e.message.usage.output;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const ameType = e.assistantMessageEvent?.type;
|
|
66
|
+
if (ameType) {
|
|
67
|
+
if (ameType.startsWith("thinking")) {
|
|
68
|
+
phase = "Thinking";
|
|
69
|
+
} else if (ameType === "text_delta") {
|
|
70
|
+
phase = "Generating";
|
|
71
|
+
} else if (ameType.startsWith("tool")) {
|
|
72
|
+
phase = "Tool call";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
update(ctx);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
80
|
+
cleanup(ctx);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
84
|
+
cleanup(ctx);
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export default extension;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DCL Tasks Extension
|
|
3
|
+
*
|
|
4
|
+
* Provides the `tasks` tool (LLM-callable) for listing/stopping background tasks,
|
|
5
|
+
* and the `/tasks` command for interactive management via a shared registry.
|
|
6
|
+
* Also maintains a footer status indicator and cleans up on shutdown.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExtensionFactory } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { StringEnum } from "@mariozechner/pi-ai";
|
|
11
|
+
import { Type } from "@sinclair/typebox";
|
|
12
|
+
import { processes } from "./process-registry.js";
|
|
13
|
+
|
|
14
|
+
function updateStatus(ctx: { ui: { setStatus(key: string, text: string | undefined): void } }): void {
|
|
15
|
+
const names = Array.from(processes.values()).map((p) => p.name.toLowerCase());
|
|
16
|
+
ctx.ui.setStatus("tasks", names.length > 0 ? `▶ ${names.join(", ")}` : undefined);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function textResult(text: string) {
|
|
20
|
+
return { content: [{ type: "text" as const, text }], details: undefined };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const extension: ExtensionFactory = (pi) => {
|
|
24
|
+
pi.registerTool({
|
|
25
|
+
name: "tasks",
|
|
26
|
+
label: "Tasks",
|
|
27
|
+
description: "List or stop running background tasks (preview server, etc.).",
|
|
28
|
+
parameters: Type.Object({
|
|
29
|
+
action: StringEnum(["list", "stop"] as const, { description: "Action to perform" }),
|
|
30
|
+
name: Type.Optional(Type.String({ description: "Task name to stop (for stop action)" })),
|
|
31
|
+
}),
|
|
32
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
33
|
+
if (params.action === "list") {
|
|
34
|
+
if (processes.size === 0) {
|
|
35
|
+
return textResult("No background tasks running.");
|
|
36
|
+
}
|
|
37
|
+
const lines = Array.from(processes.entries()).map(
|
|
38
|
+
([id, p]) => `- ${id}: ${p.name}${p.info ? ` (${p.info})` : ""}`
|
|
39
|
+
);
|
|
40
|
+
return textResult(`Running tasks:\n${lines.join("\n")}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (params.action === "stop") {
|
|
44
|
+
if (!params.name) {
|
|
45
|
+
return textResult("Please specify a task name to stop.");
|
|
46
|
+
}
|
|
47
|
+
const proc = processes.get(params.name);
|
|
48
|
+
if (!proc) {
|
|
49
|
+
const available = Array.from(processes.keys()).join(", ") || "none";
|
|
50
|
+
return textResult(`Task "${params.name}" not found. Running tasks: ${available}`);
|
|
51
|
+
}
|
|
52
|
+
proc.kill();
|
|
53
|
+
processes.delete(params.name);
|
|
54
|
+
updateStatus(ctx);
|
|
55
|
+
return textResult(`${proc.name} stopped.`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return textResult(`Unknown action: ${params.action}`);
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
pi.registerCommand("tasks", {
|
|
63
|
+
description: "Manage running background tasks",
|
|
64
|
+
handler: async (_args, ctx) => {
|
|
65
|
+
if (processes.size === 0) {
|
|
66
|
+
ctx.ui.notify("No background tasks running.", "info");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const entries = Array.from(processes.entries());
|
|
71
|
+
const options = entries.map(([_id, p]) => (p.info ? `${p.name} — ${p.info}` : p.name));
|
|
72
|
+
options.push("Close menu");
|
|
73
|
+
|
|
74
|
+
const selected = await ctx.ui.select("Background tasks", options);
|
|
75
|
+
|
|
76
|
+
if (!selected || selected === "Close menu") return;
|
|
77
|
+
|
|
78
|
+
const idx = options.indexOf(selected);
|
|
79
|
+
if (idx < 0 || idx >= entries.length) return;
|
|
80
|
+
|
|
81
|
+
const [id, proc] = entries[idx];
|
|
82
|
+
|
|
83
|
+
const action = await ctx.ui.select(proc.name, ["Stop it", "Back"]);
|
|
84
|
+
if (action !== "Stop it") return;
|
|
85
|
+
|
|
86
|
+
proc.kill();
|
|
87
|
+
processes.delete(id);
|
|
88
|
+
ctx.ui.notify(`${proc.name} stopped.`, "info");
|
|
89
|
+
updateStatus(ctx);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
pi.on("session_shutdown", () => {
|
|
94
|
+
for (const [id, proc] of processes) {
|
|
95
|
+
proc.kill();
|
|
96
|
+
processes.delete(id);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export { updateStatus };
|
|
102
|
+
export default extension;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update check extension for OpenDCL.
|
|
3
|
+
* On session start, checks npm registry for a newer version and notifies the user.
|
|
4
|
+
* Fails silently on network errors or if the package isn't published yet.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ExtensionFactory } from "@mariozechner/pi-coding-agent";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
const FETCH_TIMEOUT_MS = 5000;
|
|
15
|
+
const PACKAGE_JSON_PATH = join(__dirname, "..", "package.json");
|
|
16
|
+
|
|
17
|
+
/** Read and parse the project's package.json. */
|
|
18
|
+
function readPackageJson(): { name?: string; version?: string } {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(readFileSync(PACKAGE_JSON_PATH, "utf-8"));
|
|
21
|
+
} catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Read the package name from package.json. */
|
|
27
|
+
export function getPackageName(): string {
|
|
28
|
+
return readPackageJson().name || "@dcl-regenesislabs/opendcl";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Read the installed version from package.json. */
|
|
32
|
+
export function getInstalledVersion(): string {
|
|
33
|
+
return readPackageJson().version || "0.0.0";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Fetch the latest published version from npm registry. */
|
|
37
|
+
export async function fetchLatestVersion(): Promise<string | null> {
|
|
38
|
+
try {
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
41
|
+
const registryUrl = `https://registry.npmjs.org/${getPackageName()}/latest`;
|
|
42
|
+
const res = await fetch(registryUrl, { signal: controller.signal });
|
|
43
|
+
clearTimeout(timeout);
|
|
44
|
+
if (!res.ok) return null;
|
|
45
|
+
const data = (await res.json()) as { version?: string };
|
|
46
|
+
return data.version || null;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Compare two semver strings. Returns true if latest is newer than current. */
|
|
53
|
+
export function isNewerVersion(current: string, latest: string): boolean {
|
|
54
|
+
const currentParts = current.split(".").map((n) => parseInt(n, 10) || 0);
|
|
55
|
+
const latestParts = latest.split(".").map((n) => parseInt(n, 10) || 0);
|
|
56
|
+
const length = Math.max(currentParts.length, latestParts.length);
|
|
57
|
+
for (let i = 0; i < length; i++) {
|
|
58
|
+
const currentPart = currentParts[i] || 0;
|
|
59
|
+
const latestPart = latestParts[i] || 0;
|
|
60
|
+
if (latestPart > currentPart) return true;
|
|
61
|
+
if (latestPart < currentPart) return false;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const extension: ExtensionFactory = (pi) => {
|
|
67
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
68
|
+
if (!ctx.hasUI) return;
|
|
69
|
+
|
|
70
|
+
// Fire-and-forget: don't block session startup
|
|
71
|
+
fetchLatestVersion().then((latest) => {
|
|
72
|
+
if (latest && isNewerVersion(getInstalledVersion(), latest)) {
|
|
73
|
+
ctx.ui.notify(`OpenDCL v${latest} is available. Run: npm install -g ${getPackageName()}`, "info");
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export default extension;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DCL Validate Extension
|
|
3
|
+
*
|
|
4
|
+
* Hooks into write tool calls — after writing .ts/.tsx files, runs
|
|
5
|
+
* `npx tsc --noEmit` to catch type errors immediately.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionFactory } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { join, dirname } from "node:path";
|
|
10
|
+
import { fileExists, findSceneRoot } from "./scene-utils.js";
|
|
11
|
+
|
|
12
|
+
const extension: ExtensionFactory = (pi) => {
|
|
13
|
+
let validationTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
14
|
+
let lastValidationTime = 0;
|
|
15
|
+
const DEBOUNCE_MS = 2000; // Don't validate more than once every 2 seconds
|
|
16
|
+
|
|
17
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
18
|
+
// Only react to write tool results for .ts/.tsx files
|
|
19
|
+
if (event.toolName !== "write" || event.isError) return;
|
|
20
|
+
|
|
21
|
+
const filePath = (event.input as { path?: string }).path ?? "";
|
|
22
|
+
if (!filePath.match(/\.tsx?$/)) return;
|
|
23
|
+
|
|
24
|
+
// Find scene root from the written file's directory
|
|
25
|
+
const fileDir = dirname(filePath);
|
|
26
|
+
const sceneRoot = await findSceneRoot(fileDir);
|
|
27
|
+
if (!sceneRoot) return;
|
|
28
|
+
|
|
29
|
+
// Check for tsconfig.json
|
|
30
|
+
if (!(await fileExists(join(sceneRoot, "tsconfig.json")))) return;
|
|
31
|
+
|
|
32
|
+
// Check for node_modules (tsc needs @dcl/sdk types)
|
|
33
|
+
if (!(await fileExists(join(sceneRoot, "node_modules")))) return;
|
|
34
|
+
|
|
35
|
+
// Debounce validation
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
if (now - lastValidationTime < DEBOUNCE_MS) return;
|
|
38
|
+
lastValidationTime = now;
|
|
39
|
+
|
|
40
|
+
// Clear any pending validation
|
|
41
|
+
if (validationTimeout) {
|
|
42
|
+
clearTimeout(validationTimeout);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Run validation after a short delay (allows multiple writes to batch)
|
|
46
|
+
validationTimeout = setTimeout(async () => {
|
|
47
|
+
try {
|
|
48
|
+
const result = await pi.exec("npx", ["tsc", "--noEmit", "--pretty"], {
|
|
49
|
+
cwd: sceneRoot,
|
|
50
|
+
timeout: 30000,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (result.code !== 0 && result.stdout) {
|
|
54
|
+
// Extract error summary
|
|
55
|
+
const errors = result.stdout
|
|
56
|
+
.split("\n")
|
|
57
|
+
.filter((line) => line.includes("error TS"))
|
|
58
|
+
.slice(0, 5) // Show max 5 errors
|
|
59
|
+
.join("\n");
|
|
60
|
+
|
|
61
|
+
if (errors) {
|
|
62
|
+
// Send validation errors as a message to the agent
|
|
63
|
+
pi.sendMessage(
|
|
64
|
+
{
|
|
65
|
+
customType: "dcl-validation",
|
|
66
|
+
content: `TypeScript validation errors detected:\n\`\`\`\n${errors}\n\`\`\`\nPlease fix these type errors.`,
|
|
67
|
+
display: `TypeScript errors found in ${filePath}`,
|
|
68
|
+
},
|
|
69
|
+
{ triggerTurn: false, deliverAs: "nextTurn" }
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Validation timed out or failed — don't block the user
|
|
75
|
+
}
|
|
76
|
+
}, 500);
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export default extension;
|