scripts / apply-1.5-toolsProvider.py
scripts / apply-1.5-toolsProvider.py
#!/usr/bin/env python3
"""Apply 1.5.x toolsProvider fixes. Run from repo root: python3 scripts/apply-1.5-toolsProvider.py"""
from pathlib import Path
import sys
path = Path("src/toolsProvider.ts")
if not path.exists():
print("Run from repo root; src/toolsProvider.ts not found")
sys.exit(1)
text = path.read_text(encoding="utf-8")
orig = text
Q = chr(34)
def block(*lines):
return "\n".join(lines)
old_resolve = block(
" const neo4jConfig = {",
" uri: pluginConfig.get(" + Q + "neo4j.uri" + Q + ").trim(),",
" username: pluginConfig.get(" + Q + "neo4j.username" + Q + ").trim(),",
" password: pluginConfig.get(" + Q + "neo4j.password" + Q + "),",
" };",
"",
" const resolveNeo4jConnection = (connectionString?: string) => {",
" const uri = connectionString?.trim() || neo4jConfig.uri;",
" if (!uri) {",
" throw new Error(",
" " + Q + "Neo4j URI is not configured. Set neo4j.uri in plugin settings or provide connection_string." + Q + ",",
" );",
" }",
" return {",
" connection_string: uri,",
" username: neo4jConfig.username || undefined,",
" password: neo4jConfig.password || undefined,",
" };",
" };",
)
new_resolve = block(
" const neo4jConfig = {",
" uri: (pluginConfig.get(" + Q + "neo4j.uri" + Q + ") || " + Q + Q + ").trim(),",
" username: (pluginConfig.get(" + Q + "neo4j.username" + Q + ") || " + Q + "neo4j" + Q + ").trim(),",
" password: pluginConfig.get(" + Q + "neo4j.password" + Q + ") ?? " + Q + Q + ",",
" };",
"",
" /** Prefer plugin settings; optional connection_string is an override only. */",
" const resolveNeo4jConnection = (connectionString?: string) => {",
" const uri = (connectionString?.trim() || neo4jConfig.uri).trim();",
" if (!uri) {",
" throw new Error(",
" " + Q + "Neo4j is not configured. Set neo4j.uri (and neo4j.password) in Timbers Toolbox plugin settings, or pass connection_string as an override." + Q + ",",
" );",
" }",
" if (!connectionString?.trim() && !neo4jConfig.password) {",
" throw new Error(",
" " + Q + "Neo4j password is empty. Set neo4j.password in Timbers Toolbox plugin settings." + Q + ",",
" );",
" }",
" return {",
" connection_string: uri,",
" username: neo4jConfig.username || " + Q + "neo4j" + Q + ",",
" password: neo4jConfig.password || undefined,",
" };",
" };",
)
if "Neo4j password is empty" not in text:
if old_resolve not in text:
print("WARN: resolveNeo4jConnection block not found")
else:
text = text.replace(old_resolve, new_resolve, 1)
print("OK: resolveNeo4jConnection")
else:
print("SKIP: resolveNeo4jConnection already patched")
old_instr = block(
" const instructionsPath = join(currentWorkingDirectory, " + Q + "SUB_AGENT_INSTRUCTIONS.md" + Q + ");",
" try {",
" const instructions = await readFile(instructionsPath, " + Q + "utf-8" + Q + ");",
" if (instructions.trim()) currentSystemPrompt = instructions;",
" } catch (e) { }",
)
new_instr = block(
" // Prefer packaged plugin instructions, then workspace copies.",
" const subAgentInstructionCandidates = [",
" join(__dirname, " + Q + ".." + Q + ", " + Q + "instructions" + Q + ", " + Q + "SUB_AGENT_INSTRUCTIONS.md" + Q + "),",
" join(__dirname, " + Q + "instructions" + Q + ", " + Q + "SUB_AGENT_INSTRUCTIONS.md" + Q + "),",
" join(dirname(__dirname), " + Q + "instructions" + Q + ", " + Q + "SUB_AGENT_INSTRUCTIONS.md" + Q + "),",
" join(currentWorkingDirectory, " + Q + "instructions" + Q + ", " + Q + "SUB_AGENT_INSTRUCTIONS.md" + Q + "),",
" join(currentWorkingDirectory, " + Q + "SUB_AGENT_INSTRUCTIONS.md" + Q + "),",
" ];",
" for (const instructionsPath of subAgentInstructionCandidates) {",
" try {",
" const instructions = await readFile(instructionsPath, " + Q + "utf-8" + Q + ");",
" if (instructions.trim()) {",
" currentSystemPrompt = instructions;",
" break;",
" }",
" } catch (e) { /* try next */ }",
" }",
)
if "subAgentInstructionCandidates" not in text:
if old_instr not in text:
print("WARN: instructionsPath block not found")
else:
text = text.replace(old_instr, new_instr, 1)
print("OK: sub-agent instructions paths")
else:
print("SKIP: sub-agent instructions already patched")
text2 = text.replace(Q + "neo4j_create_rel" + Q, Q + "neo4j_create_relationship" + Q)
text2 = text2.replace(Q + "neo4j_delete_rel" + Q, Q + "neo4j_delete_relationship" + Q)
if text2 != text:
text = text2
print("OK: neo4j short names")
old_desc = 'description: "Execute arbitrary Cypher query on Neo4j database."'
new_desc = 'description: "Execute Cypher on Neo4j using plugin neo4j.* settings (connection_string optional override). Prefer single statements; for large multi-statement scripts use neo4j_run_cypher_file with a workspace path."'
if old_desc in text:
text = text.replace(old_desc, new_desc, 1)
print("OK: neo4j_query description")
marker = block(
" tools.push(neo4jGraphAnalysisTool);",
"",
" // --- Feed Tools (3 tools) ---",
)
insert = block(
" tools.push(neo4jGraphAnalysisTool);",
"",
" const neo4jTestConnectionTool = tool({",
" name: " + Q + "neo4j_run_cypher_file".replace("run_cypher_file", "test_connection") + Q + ",",
" description: " + Q + "Verify Neo4j connectivity using plugin settings (or optional connection_string override)." + Q + ",",
" parameters: { connection_string: z.string().optional() },",
" implementation: async (p) => {",
" const c = resolveNeo4jConnection(p.connection_string);",
" return neo4jTools.neo4j_test_connection(c.connection_string, c.username, c.password);",
" },",
" });",
" tools.push(neo4jTestConnectionTool);",
"",
" const neo4jRunScriptTool = tool({",
" name: " + Q + "neo4j_run_script" + Q + ",",
" description: " + Q + "Run a multi-statement Cypher script (statements separated by semicolons). Better for bulk MERGE scripts than neo4j_query." + Q + ",",
" parameters: {",
" connection_string: z.string().optional(),",
" cypher: z.string().min(1),",
" stop_on_error: z.boolean().optional(),",
" },",
" implementation: async (p) => {",
" const c = resolveNeo4jConnection(p.connection_string);",
" return neo4jTools.neo4j_run_script(c.connection_string, p.cypher, c.username, c.password, p.stop_on_error !== false);",
" },",
" });",
" tools.push(neo4jRunScriptTool);",
"",
" const neo4jRunCypherFileTool = tool({",
" name: " + Q + "neo4j_run_cypher_file" + Q + ",",
" description: " + Q + "Load and run a .cypher file from the workspace (multi-statement bulk load). Prefer this over pasting large Cypher into neo4j_query." + Q + ",",
" parameters: {",
" connection_string: z.string().optional(),",
" file_path: z.string().min(1),",
" stop_on_error: z.boolean().optional(),",
" },",
" implementation: async (p) => {",
" const c = resolveNeo4jConnection(p.connection_string);",
" const abs = validatePath(currentWorkingDirectory, p.file_path);",
" const content = await readFile(abs, " + Q + "utf-8" + Q + ");",
" return neo4jTools.neo4j_run_script(c.connection_string, content, c.username, c.password, p.stop_on_error !== false);",
" },",
" });",
" tools.push(neo4jRunCypherFileTool);",
"",
" // --- Feed Tools (3 tools) ---",
)
# Fix accidental replace in name above - ensure test_connection name is correct
insert = insert.replace(
'name: " + Q + "neo4j_run_cypher_file".replace("run_cypher_file", "test_connection") + Q + ",',
'name: " + Q + "neo4j_test_connection" + Q + ",',
)
if "neo4j_run_cypher_file" not in text:
if marker not in text:
print("WARN: Feed Tools marker not found")
else:
text = text.replace(marker, insert, 1)
print("OK: bulk Neo4j tools registered")
else:
print("SKIP: bulk Neo4j tools already present")
old_g1 = Q + "neo4j_path_find" + Q + ", " + Q + "neo4j_graph_analysis" + Q + "] },"
new_g1 = (
Q + "neo4j_path_find" + Q + ", " + Q + "neo4j_graph_analysis" + Q + ", "
+ Q + "neo4j_test_connection" + Q + ", " + Q + "neo4j_run_script" + Q + ", "
+ Q + "neo4j_run_cypher_file" + Q + "] },"
)
if "neo4j_run_cypher_file" + Q + "] }" not in text and old_g1 in text:
text = text.replace(old_g1, new_g1, 1)
print("OK: group list")
# Clearer disabled-tool errors
old_safe_start = "const createSafeToolImplementation = <TParameters, TReturn>("
if "TOOL_PERMISSION_HINTS" not in text and old_safe_start in text:
old_safe = block(
"const createSafeToolImplementation = <TParameters, TReturn>(",
" originalImplementation: (params: TParameters) => Promise<TReturn>,",
" isEnabled: boolean,",
" toolName: string,",
") => async (params: TParameters): Promise<TReturn> => {",
" if (!isEnabled) {",
" throw new Error(`Tool '${toolName}' is disabled in the plugin settings. Please ask the user to enable 'Allow ${toolName.replace(/_/g, " + Q + " " + Q + ")}' (or similar) in the settings.`);",
" }",
" return originalImplementation(params);",
"};",
)
# Use a simpler match: find the throw line pattern
import re
pat = re.compile(
r"const createSafeToolImplementation = <TParameters, TReturn>\([\s\S]*?\};",
re.M,
)
m = pat.search(text)
if m and "TOOL_PERMISSION_HINTS" not in m.group(0):
new_safe = '''const TOOL_PERMISSION_HINTS: Record<string, string> = {
execute_command: "Plugins \u2192 Timbers Toolbox \u2192 enable \\"Allow Shell Commands\\" (execution.allowShellCommandExecution)",
run_background_command: "Plugins \u2192 Timbers Toolbox \u2192 enable \\"Allow Shell Commands\\"",
run_test_command: "Plugins \u2192 Timbers Toolbox \u2192 enable \\"Allow Shell Commands\\"",
run_in_terminal: "Plugins \u2192 Timbers Toolbox \u2192 enable \\"Allow Terminal Execution\\"",
run_python: "Plugins \u2192 Timbers Toolbox \u2192 enable \\"Allow Python Execution\\"",
run_javascript: "Plugins \u2192 Timbers Toolbox \u2192 enable \\"Allow JavaScript Execution\\"",
browser_session_open: "Plugins \u2192 Timbers Toolbox \u2192 enable \\"Allow Browser Control\\"",
browser_session_control: "Plugins \u2192 Timbers Toolbox \u2192 enable \\"Allow Browser Control\\"",
browser_session_close: "Plugins \u2192 Timbers Toolbox \u2192 enable \\"Allow Browser Control\\"",
browser_open_page: "Plugins \u2192 Timbers Toolbox \u2192 enable \\"Allow Browser Control\\"",
};
const createSafeToolImplementation = <TParameters, TReturn>(
originalImplementation: (params: TParameters) => Promise<TReturn>,
isEnabled: boolean,
toolName: string,
) => async (params: TParameters): Promise<TReturn> => {
if (!isEnabled) {
const hint =
TOOL_PERMISSION_HINTS[toolName] ||
`Plugins \u2192 Timbers Toolbox \u2192 enable the permission for '${toolName}' (or \\"Allow All Code Execution\\").`;
throw new Error(`Tool '${toolName}' is disabled. ${hint}`);
}
return originalImplementation(params);
};'''
text = text[:m.start()] + new_safe + text[m.end():]
print("OK: clearer permission errors")
else:
print("WARN: could not patch createSafeToolImplementation")
else:
if "TOOL_PERMISSION_HINTS" in text:
print("SKIP: permission hints already present")
if text == orig:
print("No changes written (already up to date or patterns drifted)")
else:
path.write_text(text, encoding="utf-8")
print("Wrote", path, len(text), "bytes")
#!/usr/bin/env python3
"""Apply 1.5.x toolsProvider fixes. Run from repo root: python3 scripts/apply-1.5-toolsProvider.py"""
from pathlib import Path
import sys
path = Path("src/toolsProvider.ts")
if not path.exists():
print("Run from repo root; src/toolsProvider.ts not found")
sys.exit(1)
text = path.read_text(encoding="utf-8")
orig = text
Q = chr(34)
def block(*lines):
return "\n".join(lines)
old_resolve = block(
" const neo4jConfig = {",
" uri: pluginConfig.get(" + Q + "neo4j.uri" + Q + ").trim(),",
" username: pluginConfig.get(" + Q + "neo4j.username" + Q + ").trim(),",
" password: pluginConfig.get(" + Q + "neo4j.password" + Q + "),",
" };",
"",
" const resolveNeo4jConnection = (connectionString?: string) => {",
" const uri = connectionString?.trim() || neo4jConfig.uri;",
" if (!uri) {",
" throw new Error(",
" " + Q + "Neo4j URI is not configured. Set neo4j.uri in plugin settings or provide connection_string." + Q + ",",
" );",
" }",
" return {",
" connection_string: uri,",
" username: neo4jConfig.username || undefined,",
" password: neo4jConfig.password || undefined,",
" };",
" };",
)
new_resolve = block(
" const neo4jConfig = {",
" uri: (pluginConfig.get(" + Q + "neo4j.uri" + Q + ") || " + Q + Q + ").trim(),",
" username: (pluginConfig.get(" + Q + "neo4j.username" + Q + ") || " + Q + "neo4j" + Q + ").trim(),",
" password: pluginConfig.get(" + Q + "neo4j.password" + Q + ") ?? " + Q + Q + ",",
" };",
"",
" /** Prefer plugin settings; optional connection_string is an override only. */",
" const resolveNeo4jConnection = (connectionString?: string) => {",
" const uri = (connectionString?.trim() || neo4jConfig.uri).trim();",
" if (!uri) {",
" throw new Error(",
" " + Q + "Neo4j is not configured. Set neo4j.uri (and neo4j.password) in Timbers Toolbox plugin settings, or pass connection_string as an override." + Q + ",",
" );",
" }",
" if (!connectionString?.trim() && !neo4jConfig.password) {",
" throw new Error(",
" " + Q + "Neo4j password is empty. Set neo4j.password in Timbers Toolbox plugin settings." + Q + ",",
" );",
" }",
" return {",
" connection_string: uri,",
" username: neo4jConfig.username || " + Q + "neo4j" + Q + ",",
" password: neo4jConfig.password || undefined,",
" };",
" };",
)
if "Neo4j password is empty" not in text:
if old_resolve not in text:
print("WARN: resolveNeo4jConnection block not found")
else:
text = text.replace(old_resolve, new_resolve, 1)
print("OK: resolveNeo4jConnection")
else:
print("SKIP: resolveNeo4jConnection already patched")
old_instr = block(
" const instructionsPath = join(currentWorkingDirectory, " + Q + "SUB_AGENT_INSTRUCTIONS.md" + Q + ");",
" try {",
" const instructions = await readFile(instructionsPath, " + Q + "utf-8" + Q + ");",
" if (instructions.trim()) currentSystemPrompt = instructions;",
" } catch (e) { }",
)
new_instr = block(
" // Prefer packaged plugin instructions, then workspace copies.",
" const subAgentInstructionCandidates = [",
" join(__dirname, " + Q + ".." + Q + ", " + Q + "instructions" + Q + ", " + Q + "SUB_AGENT_INSTRUCTIONS.md" + Q + "),",
" join(__dirname, " + Q + "instructions" + Q + ", " + Q + "SUB_AGENT_INSTRUCTIONS.md" + Q + "),",
" join(dirname(__dirname), " + Q + "instructions" + Q + ", " + Q + "SUB_AGENT_INSTRUCTIONS.md" + Q + "),",
" join(currentWorkingDirectory, " + Q + "instructions" + Q + ", " + Q + "SUB_AGENT_INSTRUCTIONS.md" + Q + "),",
" join(currentWorkingDirectory, " + Q + "SUB_AGENT_INSTRUCTIONS.md" + Q + "),",
" ];",
" for (const instructionsPath of subAgentInstructionCandidates) {",
" try {",
" const instructions = await readFile(instructionsPath, " + Q + "utf-8" + Q + ");",
" if (instructions.trim()) {",
" currentSystemPrompt = instructions;",
" break;",
" }",
" } catch (e) { /* try next */ }",
" }",
)
if "subAgentInstructionCandidates" not in text:
if old_instr not in text:
print("WARN: instructionsPath block not found")
else:
text = text.replace(old_instr, new_instr, 1)
print("OK: sub-agent instructions paths")
else:
print("SKIP: sub-agent instructions already patched")
text2 = text.replace(Q + "neo4j_create_rel" + Q, Q + "neo4j_create_relationship" + Q)
text2 = text2.replace(Q + "neo4j_delete_rel" + Q, Q + "neo4j_delete_relationship" + Q)
if text2 != text:
text = text2
print("OK: neo4j short names")
old_desc = 'description: "Execute arbitrary Cypher query on Neo4j database."'
new_desc = 'description: "Execute Cypher on Neo4j using plugin neo4j.* settings (connection_string optional override). Prefer single statements; for large multi-statement scripts use neo4j_run_cypher_file with a workspace path."'
if old_desc in text:
text = text.replace(old_desc, new_desc, 1)
print("OK: neo4j_query description")
marker = block(
" tools.push(neo4jGraphAnalysisTool);",
"",
" // --- Feed Tools (3 tools) ---",
)
insert = block(
" tools.push(neo4jGraphAnalysisTool);",
"",
" const neo4jTestConnectionTool = tool({",
" name: " + Q + "neo4j_run_cypher_file".replace("run_cypher_file", "test_connection") + Q + ",",
" description: " + Q + "Verify Neo4j connectivity using plugin settings (or optional connection_string override)." + Q + ",",
" parameters: { connection_string: z.string().optional() },",
" implementation: async (p) => {",
" const c = resolveNeo4jConnection(p.connection_string);",
" return neo4jTools.neo4j_test_connection(c.connection_string, c.username, c.password);",
" },",
" });",
" tools.push(neo4jTestConnectionTool);",
"",
" const neo4jRunScriptTool = tool({",
" name: " + Q + "neo4j_run_script" + Q + ",",
" description: " + Q + "Run a multi-statement Cypher script (statements separated by semicolons). Better for bulk MERGE scripts than neo4j_query." + Q + ",",
" parameters: {",
" connection_string: z.string().optional(),",
" cypher: z.string().min(1),",
" stop_on_error: z.boolean().optional(),",
" },",
" implementation: async (p) => {",
" const c = resolveNeo4jConnection(p.connection_string);",
" return neo4jTools.neo4j_run_script(c.connection_string, p.cypher, c.username, c.password, p.stop_on_error !== false);",
" },",
" });",
" tools.push(neo4jRunScriptTool);",
"",
" const neo4jRunCypherFileTool = tool({",
" name: " + Q + "neo4j_run_cypher_file" + Q + ",",
" description: " + Q + "Load and run a .cypher file from the workspace (multi-statement bulk load). Prefer this over pasting large Cypher into neo4j_query." + Q + ",",
" parameters: {",
" connection_string: z.string().optional(),",
" file_path: z.string().min(1),",
" stop_on_error: z.boolean().optional(),",
" },",
" implementation: async (p) => {",
" const c = resolveNeo4jConnection(p.connection_string);",
" const abs = validatePath(currentWorkingDirectory, p.file_path);",
" const content = await readFile(abs, " + Q + "utf-8" + Q + ");",
" return neo4jTools.neo4j_run_script(c.connection_string, content, c.username, c.password, p.stop_on_error !== false);",
" },",
" });",
" tools.push(neo4jRunCypherFileTool);",
"",
" // --- Feed Tools (3 tools) ---",
)
# Fix accidental replace in name above - ensure test_connection name is correct
insert = insert.replace(
'name: " + Q + "neo4j_run_cypher_file".replace("run_cypher_file", "test_connection") + Q + ",',
'name: " + Q + "neo4j_test_connection" + Q + ",',
)
if "neo4j_run_cypher_file" not in text:
if marker not in text:
print("WARN: Feed Tools marker not found")
else:
text = text.replace(marker, insert, 1)
print("OK: bulk Neo4j tools registered")
else:
print("SKIP: bulk Neo4j tools already present")
old_g1 = Q + "neo4j_path_find" + Q + ", " + Q + "neo4j_graph_analysis" + Q + "] },"
new_g1 = (
Q + "neo4j_path_find" + Q + ", " + Q + "neo4j_graph_analysis" + Q + ", "
+ Q + "neo4j_test_connection" + Q + ", " + Q + "neo4j_run_script" + Q + ", "
+ Q + "neo4j_run_cypher_file" + Q + "] },"
)
if "neo4j_run_cypher_file" + Q + "] }" not in text and old_g1 in text:
text = text.replace(old_g1, new_g1, 1)
print("OK: group list")
# Clearer disabled-tool errors
old_safe_start = "const createSafeToolImplementation = <TParameters, TReturn>("
if "TOOL_PERMISSION_HINTS" not in text and old_safe_start in text:
old_safe = block(
"const createSafeToolImplementation = <TParameters, TReturn>(",
" originalImplementation: (params: TParameters) => Promise<TReturn>,",
" isEnabled: boolean,",
" toolName: string,",
") => async (params: TParameters): Promise<TReturn> => {",
" if (!isEnabled) {",
" throw new Error(`Tool '${toolName}' is disabled in the plugin settings. Please ask the user to enable 'Allow ${toolName.replace(/_/g, " + Q + " " + Q + ")}' (or similar) in the settings.`);",
" }",
" return originalImplementation(params);",
"};",
)
# Use a simpler match: find the throw line pattern
import re
pat = re.compile(
r"const createSafeToolImplementation = <TParameters, TReturn>\([\s\S]*?\};",
re.M,
)
m = pat.search(text)
if m and "TOOL_PERMISSION_HINTS" not in m.group(0):
new_safe = '''const TOOL_PERMISSION_HINTS: Record<string, string> = {
execute_command: "Plugins \u2192 Timbers Toolbox \u2192 enable \\"Allow Shell Commands\\" (execution.allowShellCommandExecution)",
run_background_command: "Plugins \u2192 Timbers Toolbox \u2192 enable \\"Allow Shell Commands\\"",
run_test_command: "Plugins \u2192 Timbers Toolbox \u2192 enable \\"Allow Shell Commands\\"",
run_in_terminal: "Plugins \u2192 Timbers Toolbox \u2192 enable \\"Allow Terminal Execution\\"",
run_python: "Plugins \u2192 Timbers Toolbox \u2192 enable \\"Allow Python Execution\\"",
run_javascript: "Plugins \u2192 Timbers Toolbox \u2192 enable \\"Allow JavaScript Execution\\"",
browser_session_open: "Plugins \u2192 Timbers Toolbox \u2192 enable \\"Allow Browser Control\\"",
browser_session_control: "Plugins \u2192 Timbers Toolbox \u2192 enable \\"Allow Browser Control\\"",
browser_session_close: "Plugins \u2192 Timbers Toolbox \u2192 enable \\"Allow Browser Control\\"",
browser_open_page: "Plugins \u2192 Timbers Toolbox \u2192 enable \\"Allow Browser Control\\"",
};
const createSafeToolImplementation = <TParameters, TReturn>(
originalImplementation: (params: TParameters) => Promise<TReturn>,
isEnabled: boolean,
toolName: string,
) => async (params: TParameters): Promise<TReturn> => {
if (!isEnabled) {
const hint =
TOOL_PERMISSION_HINTS[toolName] ||
`Plugins \u2192 Timbers Toolbox \u2192 enable the permission for '${toolName}' (or \\"Allow All Code Execution\\").`;
throw new Error(`Tool '${toolName}' is disabled. ${hint}`);
}
return originalImplementation(params);
};'''
text = text[:m.start()] + new_safe + text[m.end():]
print("OK: clearer permission errors")
else:
print("WARN: could not patch createSafeToolImplementation")
else:
if "TOOL_PERMISSION_HINTS" in text:
print("SKIP: permission hints already present")
if text == orig:
print("No changes written (already up to date or patterns drifted)")
else:
path.write_text(text, encoding="utf-8")
print("Wrote", path, len(text), "bytes")