@dcl-regenesislabs/opendcl 0.1.4-22337368589.commit-8d0fe5a → 0.1.4-22626562583.commit-09a1d03

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 CHANGED
@@ -32,7 +32,7 @@ The result: **more creators building more scenes, faster.**
32
32
  - **Integrated commands** — `/init` to scaffold, `/preview` to launch the dev server, `/tasks` to manage running processes, `/review` to audit code
33
33
  - **TypeScript validation** — catches type errors immediately after writing code
34
34
  - **Free asset catalogs** — 2,700+ Creator Hub 3D models, 900+ CC0-licensed models, and 50 audio files the agent proactively suggests when building scenes
35
- - **Permission gate** — prompts for confirmation before destructive bash commands or writes to sensitive files
35
+ - **Permission gate** — prompts for confirmation before destructive bash commands, writes to sensitive files, or any file access outside the working directory
36
36
  - **Compact tool output** — write shows path + size instead of file content, read shows a 5-line preview instead of 10
37
37
  - **Session persistence** — pick up where you left off across sessions
38
38
 
@@ -6,10 +6,12 @@ Premium 3D models from the official Decentraland Creator Hub. All models are fre
6
6
 
7
7
  **How to use**:
8
8
  1. Find a model below that matches your scene
9
- 2. Download it: `curl -o models/filename.glb "URL"`
10
- 3. Reference it: `GltfContainer.create(entity, { src: 'models/filename.glb' })`
9
+ 2. Download it the output path **must** start with `models/`:
10
+ `curl -o models/<filename>.glb "<URL>"`
11
+ 3. Reference it: `GltfContainer.create(entity, { src: 'models/<filename>.glb' })`
11
12
 
12
- > **Important**: `GltfContainer` only works with local files. Always download models into the scene's `models/` directory first.
13
+ > **Important**: Always download into `models/`. Never write to the scene root.
14
+ > Correct: `curl -o models/Sofa_Black.glb "..."` | Wrong: `curl -o Sofa_Black.glb "..."`
13
15
 
14
16
  ---
15
17
 
@@ -6,10 +6,12 @@ Free audio files from the official Decentraland Creator Hub asset packs. All sou
6
6
 
7
7
  **How to use**:
8
8
  1. Find a sound below that matches your scene
9
- 2. Download it: `curl -o sounds/filename.mp3 "URL"`
10
- 3. Reference it: `AudioSource.create(entity, { audioClipUrl: 'sounds/filename.mp3', playing: true, loop: false })`
9
+ 2. Download it the output path **must** start with `sounds/`:
10
+ `curl -o sounds/<filename>.mp3 "<URL>"`
11
+ 3. Reference it: `AudioSource.create(entity, { audioClipUrl: 'sounds/<filename>.mp3', playing: true, loop: false })`
11
12
 
12
- > **Important**: `AudioSource` only works with local files. Always download audio into the scene's `sounds/` directory first.
13
+ > **Important**: Always download into `sounds/`. Never write to the scene root.
14
+ > Correct: `curl -o sounds/click.mp3 "..."` | Wrong: `curl -o click.mp3 "..."`
13
15
 
14
16
  ---
15
17
 
@@ -9,6 +9,15 @@ Free CC0-licensed GLB 3D models curated from the [Open Source 3D Assets](https:/
9
9
  - **Creator**: Polygonal Mind
10
10
  - **License**: CC0 (Public Domain)
11
11
 
12
+ **How to use**:
13
+ 1. Find a model below that matches your scene
14
+ 2. Download it — the output path **must** start with `models/`:
15
+ `curl -o models/<filename>.glb "<URL>"`
16
+ 3. Reference it: `GltfContainer.create(entity, { src: 'models/<filename>.glb' })`
17
+
18
+ > **Important**: Always download into `models/`. Never write to the scene root.
19
+ > Correct: `curl -o models/Tree_01.glb "..."` | Wrong: `curl -o Tree_01.glb "..."`
20
+
12
21
  Base URL for all downloads:
13
22
  ```
14
23
  https://raw.githubusercontent.com/ToxSam/cc0-models-Polygonal-Mind/main/projects/
package/dist/index.js CHANGED
@@ -51,6 +51,7 @@ const extensions = [
51
51
  "dcl-update-check.ts",
52
52
  "dcl-status.ts",
53
53
  "dcl-tasks.ts",
54
+ "dcl-asset-path.ts",
54
55
  ];
55
56
  for (const ext of extensions) {
56
57
  args.push("-e", join(extDir, ext));
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;GAIG;AAEH,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AACtC,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;AAEzC,uFAAuF;AACvF,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;AACtD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,CAAC;IACrC,OAAO,CAAC,GAAG,CAAC,mBAAmB,GAAG,QAAQ,CAAC;AAC7C,CAAC;AAED,wEAAwE;AACxE,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;AACrD,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;IAC9B,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,aAAa,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,iBAAiB,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AAC3F,CAAC;AAED,yCAAyC;AACzC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,wFAAwF;AACxF,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC;IACtC,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,UAAU,EAAE,mBAAmB,CAAC,EAAE,OAAO,CAAC,CAAC;IACzE,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IAC/C,MAAM,YAAY,GAAG,GAAG;SACrB,OAAO,CAAC,uBAAuB,EAAE,EAAE,CAAC;SACpC,OAAO,CAAC,wBAAwB,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;SAC9E,IAAI,EAAE,CAAC;IACV,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,YAAY,CAAC,CAAC;AAC7C,CAAC;AAED,sBAAsB;AACtB,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;AAC9C,MAAM,UAAU,GAAG;IACjB,gBAAgB;IAChB,gBAAgB;IAChB,aAAa;IACb,eAAe;IACf,cAAc;IACd,qBAAqB;IACrB,iBAAiB;IACjB,eAAe;IACf,qBAAqB;IACrB,eAAe;IACf,cAAc;CACf,CAAC;AACF,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;IAC7B,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;AACrC,CAAC;AACD,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC;AACpD,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC,CAAC;AAEtD,6BAA6B;AAC7B,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAC;AAEjD,yFAAyF;AACzF,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,IAAI,CAAC,UAAU,EAAE,mBAAmB,CAAC,CAAC,CAAC;AACtE,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,IAAI,CAAC,UAAU,EAAE,oBAAoB,CAAC,CAAC,CAAC;AAEvE,gFAAgF;AAChF,mEAAmE;AACnE,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC;IACb,OAAO,CAAC,GAAG,CAAC,qBAAqB,GAAG,GAAG,CAAC;AAC1C,CAAC;AAED,6EAA6E;AAC7E,sEAAsE;AACtE,MAAM,YAAY,GAAG,eAAe,CAAC,SAAS,CAAC,WAAW,CAAC;AAC3D,eAAe,CAAC,SAAS,CAAC,WAAW,GAAG,UAAU,GAAW;IAC3D,IAAI,GAAG,CAAC,UAAU,CAAC,qBAAqB,CAAC;QAAE,OAAO;IAClD,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AAC/B,CAAC,CAAC;AAEF,+EAA+E;AAC/E,qDAAqD;AACpD,eAAe,CAAC,SAAiB,CAAC,sBAAsB,GAAG;IAC1D,OAAO,SAAS,CAAC;AACnB,CAAC,CAAC;AAEF,0EAA0E;AAC1E,iFAAiF;AACjF,gEAAgE;AAChE,MAAM,cAAc,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;AAClG,eAAe,CAAC,SAAiB,CAAC,sBAAsB,GAAG;IACzD,IAAY,CAAC,UAAU,CACtB,YAAY,cAAc,gEAAgE,cAAc,EAAE,CAC3G,CAAC;AACJ,CAAC,CAAC;AAEF,yFAAyF;AACzF,uFAAuF;AACvF,8FAA8F;AAC9F,2EAA2E;AAC3E,MAAM,WAAW,GAAI,eAAe,CAAC,SAAiB,CAAC,2BAA2B,CAAC;AAClF,eAAe,CAAC,SAAiB,CAAC,2BAA2B,GAAG,UAAU,QAAgB;IACzF,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAClD,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC9B,OAAO,wBAAwB,CAAC,QAAQ,CAAC,CAAC;AAC5C,CAAC,CAAC;AAEF,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACvB,OAAO,CAAC,KAAK,CAAC,sBAAsB,EAAE,GAAG,CAAC,CAAC;IAC3C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Asset Path Guard Extension
3
+ *
4
+ * Blocks curl downloads that place asset files in the wrong directory.
5
+ * Models (.glb/.gltf) must go into models/, audio (.mp3/.ogg/.wav) into sounds/.
6
+ */
7
+
8
+ import type { ExtensionFactory } from "@mariozechner/pi-coding-agent";
9
+
10
+ interface AssetViolation {
11
+ file: string;
12
+ expected: string;
13
+ }
14
+
15
+ const ASSET_RULES: { pattern: RegExp; dir: string }[] = [
16
+ { pattern: /\.gl(?:b|tf)$/i, dir: "models/" },
17
+ { pattern: /\.(?:mp3|ogg|wav)$/i, dir: "sounds/" },
18
+ ];
19
+
20
+ /**
21
+ * Check a curl command for asset files downloaded to the wrong directory.
22
+ * Returns null if the command is fine, or a violation object if it should be blocked.
23
+ */
24
+ export function checkCurlOutput(command: string): AssetViolation | null {
25
+ const outputMatches = command.matchAll(/-o\s+["']?([^\s"']+)/g);
26
+ for (const match of outputMatches) {
27
+ const outputPath = match[1];
28
+ for (const rule of ASSET_RULES) {
29
+ if (rule.pattern.test(outputPath) && !outputPath.includes(rule.dir)) {
30
+ const relativePath = outputPath.replace(/^\.\//, "");
31
+ return {
32
+ file: relativePath,
33
+ expected: rule.dir + relativePath,
34
+ };
35
+ }
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+
41
+ const extension: ExtensionFactory = (pi) => {
42
+ pi.on("tool_call", async (event) => {
43
+ if (event.toolName !== "bash") return;
44
+
45
+ const command = (event.input as { command?: string }).command ?? "";
46
+ const violation = checkCurlOutput(command);
47
+ if (!violation) return;
48
+
49
+ return {
50
+ block: true,
51
+ reason: [
52
+ "Asset files must be downloaded into the correct directory.",
53
+ `Use: curl -o ${violation.expected} instead of curl -o ${violation.file}`,
54
+ "Models (.glb/.gltf) → models/ Audio (.mp3/.ogg/.wav) → sounds/",
55
+ ].join("\n"),
56
+ };
57
+ });
58
+ };
59
+
60
+ export default extension;
@@ -7,25 +7,53 @@
7
7
  */
8
8
 
9
9
  import type { ExtensionFactory } from "@mariozechner/pi-coding-agent";
10
- import { classifyBashCommand, classifyFilePath } from "./utils.js";
10
+ import { classifyBashCommand, classifyFilePath, isOutsideCwd, OUTSIDE_CWD_REASON } from "./utils.js";
11
+ import { resolve } from "node:path";
11
12
 
12
- function blockResult(reason: string, detail: string): { block: true; reason: string } {
13
+ type BlockResult = { block: true; reason: string };
14
+
15
+ function blockResult(reason: string, detail: string): BlockResult {
13
16
  return {
14
17
  block: true,
15
18
  reason: `Blocked: ${reason}\n${detail}\nUse --no-permissions to allow in non-interactive mode.`,
16
19
  };
17
20
  }
18
21
 
19
- function denyResult(reason: string): { block: true; reason: string } {
22
+ function denyResult(reason: string): BlockResult {
20
23
  return { block: true, reason: `User denied: ${reason}` };
21
24
  }
22
25
 
23
- const ALLOW = "Allow";
24
- const ALWAYS = "Always allow";
25
- const DENY = "Deny";
26
+ const CHOICES = ["Allow", "Always allow", "Deny"] as const;
26
27
 
27
28
  const extension: ExtensionFactory = (pi) => {
28
29
  const sessionAllow = new Set<string>();
30
+ const allowedPaths = new Set<string>();
31
+
32
+ function isPathAllowed(resolvedPath: string): boolean {
33
+ for (const allowed of allowedPaths) {
34
+ if (resolvedPath === allowed || resolvedPath.startsWith(allowed + "/")) return true;
35
+ }
36
+ return false;
37
+ }
38
+
39
+ /**
40
+ * Prompts the user for confirmation and handles their choice.
41
+ * Returns a BlockResult to deny, or undefined to allow.
42
+ */
43
+ async function promptOrBlock(
44
+ ctx: { hasUI: boolean; ui: { select: (title: string, options: string[]) => Promise<string | null> } },
45
+ reason: string,
46
+ detail: string,
47
+ onAlways: () => void,
48
+ ): Promise<BlockResult | undefined> {
49
+ if (!ctx.hasUI) return blockResult(reason, detail);
50
+
51
+ const choice = await ctx.ui.select(`${reason}\n${detail}`, [...CHOICES]);
52
+
53
+ if (choice === "Always allow") { onAlways(); return; }
54
+ if (choice === "Allow") return;
55
+ return denyResult(reason);
56
+ }
29
57
 
30
58
  pi.registerFlag("no-permissions", {
31
59
  description: "Disable permission gate (skip confirmation prompts for dangerous operations)",
@@ -43,33 +71,36 @@ const extension: ExtensionFactory = (pi) => {
43
71
  const reason = classifyBashCommand(command);
44
72
  if (!reason || sessionAllow.has(reason)) return;
45
73
 
46
- if (!ctx.hasUI) return blockResult(reason, `Command: ${command}`);
47
-
48
- const choice = await ctx.ui.select(
49
- `${reason}\nCommand: ${command}`,
50
- [ALLOW, ALWAYS, DENY],
51
- );
52
-
53
- if (choice === ALWAYS) { sessionAllow.add(reason); return; }
54
- if (choice === ALLOW) return;
55
- return denyResult(reason);
74
+ return promptOrBlock(ctx, reason, `Command: ${command}`, () => sessionAllow.add(reason));
56
75
  }
57
76
 
58
77
  if (toolName === "write" || toolName === "edit") {
59
78
  const filePath = (event.input as { path?: string }).path ?? "";
60
79
  const reason = filePath ? classifyFilePath(filePath, ctx.cwd) : null;
61
- if (!reason || sessionAllow.has(reason)) return;
80
+ if (!reason) return;
62
81
 
63
- if (!ctx.hasUI) return blockResult(reason, `Path: ${filePath}`);
82
+ if (reason === OUTSIDE_CWD_REASON) {
83
+ const resolved = resolve(ctx.cwd, filePath);
84
+ if (isPathAllowed(resolved)) return;
85
+
86
+ return promptOrBlock(ctx, reason, `File: ${filePath}`, () => allowedPaths.add(resolved));
87
+ }
88
+
89
+ if (sessionAllow.has(reason)) return;
90
+
91
+ return promptOrBlock(ctx, reason, `Path: ${filePath}`, () => sessionAllow.add(reason));
92
+ }
93
+
94
+ if (toolName === "read" || toolName === "grep" || toolName === "find" || toolName === "ls") {
95
+ const filePath = (event.input as { path?: string }).path ?? "";
96
+ if (!filePath) return;
64
97
 
65
- const choice = await ctx.ui.select(
66
- `${reason}\nFile: ${filePath}`,
67
- [ALLOW, ALWAYS, DENY],
68
- );
98
+ const resolved = resolve(ctx.cwd, filePath);
99
+ const reason = isOutsideCwd(filePath, ctx.cwd);
100
+ if (!reason) return;
101
+ if (isPathAllowed(resolved)) return;
69
102
 
70
- if (choice === ALWAYS) { sessionAllow.add(reason); return; }
71
- if (choice === ALLOW) return;
72
- return denyResult(reason);
103
+ return promptOrBlock(ctx, reason, `Path: ${filePath}`, () => allowedPaths.add(resolved));
73
104
  }
74
105
  });
75
106
  };
@@ -5,6 +5,8 @@
5
5
 
6
6
  import { resolve, relative } from "node:path";
7
7
 
8
+ export const OUTSIDE_CWD_REASON = "Accesses path outside working directory";
9
+
8
10
  interface DenylistEntry {
9
11
  pattern: RegExp;
10
12
  reason: string;
@@ -75,8 +77,20 @@ export function classifyFilePath(filePath: string, projectRoot: string): string
75
77
  const rel = relative(projectRoot, resolved);
76
78
 
77
79
  if (rel.startsWith("..")) {
78
- return "File is outside the project root";
80
+ return OUTSIDE_CWD_REASON;
79
81
  }
80
82
 
81
83
  return findMatchingReason(SENSITIVE_FILE_PATTERNS, filePath);
82
84
  }
85
+
86
+ /**
87
+ * Returns a reason string if the file path resolves outside the given cwd,
88
+ * or null if inside (or empty).
89
+ */
90
+ export function isOutsideCwd(filePath: string, cwd: string): string | null {
91
+ if (!filePath) return null;
92
+ const resolved = resolve(cwd, filePath);
93
+ const rel = relative(cwd, resolved);
94
+ if (rel.startsWith("..")) return OUTSIDE_CWD_REASON;
95
+ return null;
96
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcl-regenesislabs/opendcl",
3
- "version": "0.1.4-22337368589.commit-8d0fe5a",
3
+ "version": "0.1.4-22626562583.commit-09a1d03",
4
4
  "description": "AI coding assistant for Decentraland SDK7 scene development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -66,5 +66,5 @@
66
66
  "prompts/",
67
67
  "context/"
68
68
  ],
69
- "commit": "8d0fe5a0f4b0f42d764cb734512ce31a1dabb8a4"
69
+ "commit": "09a1d037c3a42a9041ce7baccd6d22411cb365ed"
70
70
  }