Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion extensions/roopik-roo/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ logs
.qodo/
.vercel
.roo/mcp.json
.dio/mcp.json

# Qdrant
qdrant_storage/
Expand All @@ -59,4 +60,4 @@ plans/

# Keep config files (if they need to be committed, uncomment these lines)
# !vitest.config.js
# !*.config.js
# !*.config.js
28 changes: 13 additions & 15 deletions extensions/roopik-roo/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "roodio",
"displayName": "RooDio",
"displayName": "Dio",
"description": "AI Coding Agent with Roopik IDE Integration",
"publisher": "roopik",
"version": "3.37.1",
Expand All @@ -14,7 +14,7 @@
"node": "20.19.2"
},
"author": {
"name": "Roo Dio"
"name": "Dio"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -42,13 +42,11 @@
"sonnet",
"ai",
"llama",
"roo code",
"roocode",
"roopik",
"dio code",
"dio-code",
"roopik-labs",
"roopik-ide",
"roopik-dio",
"roo-dio",
"dio-ide",
"dio-dio",
"dio"
],
"activationEvents": [
Expand All @@ -62,14 +60,14 @@
{
"id": "roodio-ActivityBar",
"title": "%views.activitybar.title%",
"icon": "dist/assets/icons/DIO_icon.svg"
"icon": "dist/assets/icons/icon.svg"
}
],
"secondarySidebar": [
{
"id": "roodio-AuxiliaryBar",
"title": "Roo Dio Agent",
"icon": "dist/assets/icons/DIO_icon.svg"
"title": "Roopik Dio Agent",
"icon": "dist/assets/icons/icon.svg"
}
]
},
Expand All @@ -92,11 +90,11 @@
"commands": [
{
"command": "roodio.openChatPanel",
"title": "Open Roo Dio Chat",
"category": "Roo Dio",
"title": "Open Roopik Dio Chat",
"category": "Roopik Dio",
"icon": {
"light": "dist/assets/icons/DIO_icon.svg",
"dark": "dist/assets/icons/DIO_icon.svg"
"light": "dist/assets/icons/dio-icon-light.svg",
"dark": "dist/assets/icons/dio-icon-dark.svg"
}
},
{
Expand Down
18 changes: 9 additions & 9 deletions extensions/roopik-roo/package.nls.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"extension.displayName": "Roo Dio",
"extension.description": "A whole dev team of AI agents in your editor.",
"views.contextMenu.label": "Roo Dio",
"views.terminalMenu.label": "Roo Dio",
"views.activitybar.title": "Roo Dio",
"views.sidebar.name": "Roo Dio",
"extension.displayName": "Dio",
"extension.description": "AI Coding Agent with Roopik IDE Integration",
"views.contextMenu.label": "Dio",
"views.terminalMenu.label": "Dio",
"views.activitybar.title": "Dio",
"views.sidebar.name": "Dio",
"command.newTask.title": "New Task",
"command.history.title": "Task History",
"command.marketplace.title": "Marketplace",
Expand All @@ -25,7 +25,7 @@
"command.terminal.explainCommand.title": "Explain This Command",
"command.acceptInput.title": "Accept Input/Suggestion",
"command.toggleAutoApprove.title": "Toggle Auto-Approve",
"configuration.title": "Roo Dio",
"configuration.title": "Dio",
"commands.allowedCommands.description": "Commands that can be auto-executed when 'Always approve execute operations' is enabled",
"commands.deniedCommands.description": "Command prefixes that will be automatically denied without asking for approval. In case of conflicts with allowed commands, the longest prefix match takes precedence. Add * to deny all commands.",
"commands.commandExecutionTimeout.description": "Maximum time in seconds to wait for command execution to complete before timing out (0 = no timeout, 1-600s, default: 0s)",
Expand All @@ -35,8 +35,8 @@
"settings.vsCodeLmModelSelector.vendor.description": "The vendor of the language model (e.g. copilot)",
"settings.vsCodeLmModelSelector.family.description": "The family of the language model (e.g. gpt-4)",
"settings.customStoragePath.description": "Custom storage path. Leave empty to use the default location. Supports absolute paths (e.g. 'D:\\RooCodeStorage')",
"settings.enableCodeActions.description": "Enable Roo Code quick fixes",
"settings.autoImportSettingsPath.description": "Path to a RooCode configuration file to automatically import on extension startup. Supports absolute paths and paths relative to the home directory (e.g. '~/Documents/roo-code-settings.json'). Leave empty to disable auto-import.",
"settings.enableCodeActions.description": "Enable Dio quick fixes",
"settings.autoImportSettingsPath.description": "Path to a Dio configuration file to automatically import on extension startup. Supports absolute paths and paths relative to the home directory (e.g. '~/Documents/roo-code-settings.json'). Leave empty to disable auto-import.",
"settings.maximumIndexedFilesForFileSearch.description": "Maximum number of files to index for the @ file search feature. Higher values provide better search results in large projects but may use more memory. Default: 10,000.",
"settings.useAgentRules.description": "Enable loading of AGENTS.md files for agent-specific rules (see https://agent-rules.org/)",
"settings.apiRequestTimeout.description": "Maximum time in seconds to wait for API responses (0 = no timeout, 1-3600s, default: 600s). Higher values are recommended for local providers like LM Studio and Ollama that may need more processing time.",
Expand Down
5 changes: 5 additions & 0 deletions extensions/roopik-roo/packages/build/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dist

# Prevent compiled JS in source folder
src/**/*.js
src/**/*.js.map
5 changes: 5 additions & 0 deletions extensions/roopik-roo/packages/cloud/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dist

# Prevent compiled JS in source folder
src/**/*.js
src/**/*.js.map
5 changes: 5 additions & 0 deletions extensions/roopik-roo/packages/core/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dist

# Prevent compiled JS in source folder
src/**/*.js
src/**/*.js.map
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from "fs"
import os from "os"
import path from "path"

import { getEsbuildScriptPath, runEsbuild } from "../esbuild-runner.js"
import { getEsbuildScriptPath, runEsbuild, NODE_BUILTIN_MODULES, COMMONJS_REQUIRE_BANNER } from "../esbuild-runner.js"

describe("getEsbuildScriptPath", () => {
it("should find esbuild-wasm script in node_modules in development", () => {
Expand Down Expand Up @@ -153,4 +153,101 @@ describe("runEsbuild", () => {
// File should be created successfully.
expect(fs.existsSync(outputFile)).toBe(true)
}, 30000)

it("should keep external modules as imports instead of bundling", async () => {
const inputFile = path.join(tempDir, "input.ts")
const outputFile = path.join(tempDir, "output.mjs")

// Write code that imports fs (a Node.js built-in).
fs.writeFileSync(
inputFile,
`
import fs from "fs"
export function fileExists(p: string): boolean {
return fs.existsSync(p)
}
`,
)

await runEsbuild({
entryPoint: inputFile,
outfile: outputFile,
format: "esm",
bundle: true,
external: ["fs"],
})

const outputContent = fs.readFileSync(outputFile, "utf-8")
// fs should remain as an import, not bundled.
expect(outputContent).toMatch(/import.*from\s*["']fs["']/)
}, 30000)

it("should add banner code when specified", async () => {
const inputFile = path.join(tempDir, "input.ts")
const outputFile = path.join(tempDir, "output.mjs")

fs.writeFileSync(inputFile, `export const greeting = "Hello"`)

const customBanner = "// This is a custom banner comment"
await runEsbuild({
entryPoint: inputFile,
outfile: outputFile,
format: "esm",
banner: customBanner,
})

const outputContent = fs.readFileSync(outputFile, "utf-8")
// Banner should be at the start of the file.
expect(outputContent.startsWith(customBanner)).toBe(true)
}, 30000)

it("should add CommonJS require shim banner for ESM bundles", async () => {
const inputFile = path.join(tempDir, "input.ts")
const outputFile = path.join(tempDir, "output.mjs")

fs.writeFileSync(inputFile, `export const value = 42`)

await runEsbuild({
entryPoint: inputFile,
outfile: outputFile,
format: "esm",
banner: COMMONJS_REQUIRE_BANNER,
})

const outputContent = fs.readFileSync(outputFile, "utf-8")
// Should contain the createRequire shim.
expect(outputContent).toContain("createRequire")
expect(outputContent).toContain("import.meta.url")
}, 30000)
})

describe("NODE_BUILTIN_MODULES", () => {
it("should include common Node.js built-in modules", () => {
expect(NODE_BUILTIN_MODULES).toContain("fs")
expect(NODE_BUILTIN_MODULES).toContain("path")
expect(NODE_BUILTIN_MODULES).toContain("crypto")
expect(NODE_BUILTIN_MODULES).toContain("http")
expect(NODE_BUILTIN_MODULES).toContain("https")
expect(NODE_BUILTIN_MODULES).toContain("os")
expect(NODE_BUILTIN_MODULES).toContain("child_process")
expect(NODE_BUILTIN_MODULES).toContain("stream")
expect(NODE_BUILTIN_MODULES).toContain("util")
expect(NODE_BUILTIN_MODULES).toContain("events")
})

it("should be an array of strings", () => {
expect(Array.isArray(NODE_BUILTIN_MODULES)).toBe(true)
expect(NODE_BUILTIN_MODULES.every((m) => typeof m === "string")).toBe(true)
})
})

describe("COMMONJS_REQUIRE_BANNER", () => {
it("should provide createRequire shim", () => {
expect(COMMONJS_REQUIRE_BANNER).toContain("createRequire")
expect(COMMONJS_REQUIRE_BANNER).toContain("import.meta.url")
})

it("should define require variable", () => {
expect(COMMONJS_REQUIRE_BANNER).toMatch(/var require\s*=/)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type { CustomToolDefinition, SerializedCustomToolDefinition, CustomToolPa

import type { StoredCustomTool, LoadResult } from "./types.js"
import { serializeCustomTool } from "./serialize.js"
import { runEsbuild } from "./esbuild-runner.js"
import { runEsbuild, NODE_BUILTIN_MODULES, COMMONJS_REQUIRE_BANNER } from "./esbuild-runner.js"

export interface RegistryOptions {
/** Directory for caching compiled TypeScript files. */
Expand Down Expand Up @@ -236,16 +236,22 @@ export class CustomToolRegistry {

/**
* Clear the TypeScript compilation cache (both in-memory and on disk).
* This removes all tool-specific subdirectories and their contents.
*/
clearCache(): void {
this.tsCache.clear()

if (fs.existsSync(this.cacheDir)) {
try {
const files = fs.readdirSync(this.cacheDir)
for (const file of files) {
if (file.endsWith(".mjs")) {
fs.unlinkSync(path.join(this.cacheDir, file))
const entries = fs.readdirSync(this.cacheDir, { withFileTypes: true })
for (const entry of entries) {
const entryPath = path.join(this.cacheDir, entry.name)
if (entry.isDirectory()) {
// Remove tool-specific subdirectory and all its contents.
fs.rmSync(entryPath, { recursive: true, force: true })
} else if (entry.name.endsWith(".mjs")) {
// Also clean up any legacy flat .mjs files from older cache format.
fs.unlinkSync(entryPath)
}
}
} catch (error) {
Expand All @@ -259,6 +265,11 @@ export class CustomToolRegistry {
/**
* Dynamically import a TypeScript or JavaScript file.
* TypeScript files are transpiled on-the-fly using esbuild.
*
* For TypeScript files, esbuild bundles the code with these considerations:
* - Node.js built-in modules (fs, path, etc.) are kept external
* - npm packages are bundled with a CommonJS shim for require() compatibility
* - The tool's local node_modules is included in the resolution path
*/
private async import(filePath: string): Promise<Record<string, CustomToolDefinition>> {
const absolutePath = path.resolve(filePath)
Expand All @@ -277,19 +288,31 @@ export class CustomToolRegistry {
return import(`file://${cachedPath}`)
}

// Ensure cache directory exists.
fs.mkdirSync(this.cacheDir, { recursive: true })

const hash = createHash("sha256").update(cacheKey).digest("hex").slice(0, 16)
const tempFile = path.join(this.cacheDir, `${hash}.mjs`)

// Use a tool-specific subdirectory to avoid .env file conflicts between tools.
const toolCacheDir = path.join(this.cacheDir, hash)
fs.mkdirSync(toolCacheDir, { recursive: true })

const tempFile = path.join(toolCacheDir, "bundle.mjs")

// Check if we have a cached version on disk (from a previous run/instance).
if (fs.existsSync(tempFile)) {
this.tsCache.set(cacheKey, tempFile)
return import(`file://${tempFile}`)
}

// Get the tool's directory to include its node_modules in resolution path.
const toolDir = path.dirname(absolutePath)
const toolNodeModules = path.join(toolDir, "node_modules")

// Combine default nodePaths with tool-specific node_modules.
// Tool's node_modules takes priority (listed first).
const nodePaths = fs.existsSync(toolNodeModules) ? [toolNodeModules, ...this.nodePaths] : this.nodePaths

// Bundle the TypeScript file with dependencies using esbuild CLI.
// - Node.js built-ins are external (they can't be bundled and are always available)
// - npm packages are bundled with CommonJS require() shim for compatibility
await runEsbuild(
{
entryPoint: absolutePath,
Expand All @@ -300,15 +323,54 @@ export class CustomToolRegistry {
bundle: true,
sourcemap: "inline",
packages: "bundle",
nodePaths: this.nodePaths,
nodePaths,
external: NODE_BUILTIN_MODULES,
banner: COMMONJS_REQUIRE_BANNER,
},
this.extensionPath,
)

// Copy .env files from the tool's source directory to the tool-specific cache directory.
// This allows tools that use dotenv with __dirname to find their .env files,
// while ensuring different tools' .env files don't overwrite each other.
this.copyEnvFiles(toolDir, toolCacheDir)

this.tsCache.set(cacheKey, tempFile)
return import(`file://${tempFile}`)
}

/**
* Copy .env files from the tool's source directory to the tool-specific cache directory.
* This allows tools that use dotenv with __dirname to find their .env files,
* while ensuring different tools' .env files don't overwrite each other.
*
* @param toolDir - The directory containing the tool source files
* @param destDir - The tool-specific cache directory to copy .env files to
*/
private copyEnvFiles(toolDir: string, destDir: string): void {
try {
const files = fs.readdirSync(toolDir)
const envFiles = files.filter((f) => f === ".env" || f.startsWith(".env."))

for (const envFile of envFiles) {
const srcPath = path.join(toolDir, envFile)
const destPath = path.join(destDir, envFile)

// Only copy if source is a file (not a directory).
const stat = fs.statSync(srcPath)
if (stat.isFile()) {
fs.copyFileSync(srcPath, destPath)
console.log(`[CustomToolRegistry] copied ${envFile} to tool cache directory`)
}
}
} catch (error) {
// Non-fatal: log but don't fail if we can't copy env files.
console.warn(
`[CustomToolRegistry] failed to copy .env files: ${error instanceof Error ? error.message : String(error)}`,
)
}
}

/**
* Check if a value is a Zod schema by looking for the _def property
* which is present on all Zod types.
Expand Down
Loading
Loading