droid-mode 0.0.9 → 0.0.11
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
-
import { ensureDir, nowIsoCompact, safeIdentifier, writeJson, getDroidModeDataDir } from "./util.mjs";
|
|
3
|
+
import { ensureDir, nowIsoCompact, safeIdentifier, uniqueSafeToolMap, writeJson, getDroidModeDataDir, serverNameToDirName } from "./util.mjs";
|
|
4
4
|
|
|
5
5
|
/** @param {string} s */
|
|
6
6
|
function toPascalCase(s) {
|
|
@@ -89,9 +89,10 @@ function schemaToTs(schema, depth = 0) {
|
|
|
89
89
|
*/
|
|
90
90
|
export function hydrateTools(opts) {
|
|
91
91
|
const ts = nowIsoCompact();
|
|
92
|
+
const serverDir = serverNameToDirName(opts.serverName);
|
|
92
93
|
const baseOut =
|
|
93
94
|
opts.outDir ||
|
|
94
|
-
path.join(getDroidModeDataDir(), "hydrated",
|
|
95
|
+
path.join(getDroidModeDataDir(), "hydrated", serverDir, ts);
|
|
95
96
|
|
|
96
97
|
ensureDir(baseOut);
|
|
97
98
|
|
|
@@ -103,11 +104,9 @@ export function hydrateTools(opts) {
|
|
|
103
104
|
if (t) selected.push(t);
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
toolmap[safe] = t?.name || safe;
|
|
110
|
-
}
|
|
107
|
+
// Build collision-free toolmap using shared function
|
|
108
|
+
const selectedNames = selected.map(t => t?.name || "tool");
|
|
109
|
+
const toolmap = uniqueSafeToolMap(selectedNames);
|
|
111
110
|
|
|
112
111
|
writeJson(path.join(baseOut, "tools.json"), selected);
|
|
113
112
|
writeJson(path.join(baseOut, "toolmap.json"), toolmap);
|
|
@@ -116,15 +115,22 @@ export function hydrateTools(opts) {
|
|
|
116
115
|
const toolmapModule = `// Auto-generated by droid-mode.\nexport default ${JSON.stringify(toolmap, null, 2)};\n`;
|
|
117
116
|
fs.writeFileSync(path.join(baseOut, "toolmap.mjs"), toolmapModule, "utf-8");
|
|
118
117
|
|
|
119
|
-
// types.d.ts
|
|
118
|
+
// types.d.ts - use toolmap keys (already collision-free)
|
|
120
119
|
const typeLines = [
|
|
121
120
|
`// Auto-generated by droid-mode (best-effort).`,
|
|
122
121
|
`// This file exists to improve IDE autocomplete; it is NOT a contract.`,
|
|
123
122
|
``,
|
|
124
123
|
];
|
|
125
124
|
|
|
125
|
+
// Build reverse map: toolName -> safeName for lookup
|
|
126
|
+
const nameToSafe = Object.create(null);
|
|
127
|
+
for (const [safe, name] of Object.entries(toolmap)) {
|
|
128
|
+
nameToSafe[name] = safe;
|
|
129
|
+
}
|
|
130
|
+
|
|
126
131
|
for (const t of selected) {
|
|
127
|
-
const
|
|
132
|
+
const toolName = t?.name || "tool";
|
|
133
|
+
const safe = nameToSafe[toolName] || safeIdentifier(toolName);
|
|
128
134
|
const pascal = toPascalCase(safe);
|
|
129
135
|
const inSchema = t?.inputSchema || t?.parameters || null;
|
|
130
136
|
const outSchema = t?.outputSchema || null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import vm from "node:vm";
|
|
4
|
-
import { nowIsoCompact, sha256Hex,
|
|
4
|
+
import { nowIsoCompact, sha256Hex, uniqueSafeToolMap, ensureDir, getDroidModeDataDir, writeJson, serverNameToDirName } from "./util.mjs";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Static, best-effort disallow list for sandboxed workflows.
|
|
@@ -100,13 +100,10 @@ export function createToolApi(opts) {
|
|
|
100
100
|
throw lastError;
|
|
101
101
|
};
|
|
102
102
|
|
|
103
|
-
//
|
|
103
|
+
// Build collision-free toolmap using shared function
|
|
104
|
+
const toolmap = uniqueSafeToolMap(opts.toolNames ?? []);
|
|
104
105
|
const t = {};
|
|
105
|
-
const toolmap
|
|
106
|
-
|
|
107
|
-
for (const toolName of opts.toolNames) {
|
|
108
|
-
const safe = safeIdentifier(toolName);
|
|
109
|
-
toolmap[safe] = toolName;
|
|
106
|
+
for (const [safe, toolName] of Object.entries(toolmap)) {
|
|
110
107
|
t[safe] = (args) => call(toolName, args);
|
|
111
108
|
}
|
|
112
109
|
|
|
@@ -200,10 +197,12 @@ export async function executeWorkflow(opts) {
|
|
|
200
197
|
export function writeRunArtifact(opts) {
|
|
201
198
|
const dataDir = getDroidModeDataDir();
|
|
202
199
|
const ts = nowIsoCompact();
|
|
203
|
-
const
|
|
200
|
+
const serverDir = serverNameToDirName(opts.serverName);
|
|
201
|
+
const outDir = path.join(dataDir, "runs", serverDir, ts);
|
|
204
202
|
ensureDir(outDir);
|
|
205
203
|
const payload = {
|
|
206
204
|
serverName: opts.serverName,
|
|
205
|
+
serverDir,
|
|
207
206
|
workflowPath: opts.workflowPath,
|
|
208
207
|
tools: opts.tools,
|
|
209
208
|
finishedAt: new Date().toISOString(),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { getDroidModeDataDir, readJsonFileIfExists, writeJson } from "./util.mjs";
|
|
2
|
+
import { getDroidModeDataDir, readJsonFileIfExists, writeJson, serverNameToDirName } from "./util.mjs";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @param {import("./mcp_client.mjs").McpClient} client
|
|
@@ -27,7 +27,8 @@ export async function fetchAllTools(client) {
|
|
|
27
27
|
*/
|
|
28
28
|
export async function getToolsCached(opts) {
|
|
29
29
|
const dataDir = getDroidModeDataDir();
|
|
30
|
-
const
|
|
30
|
+
const serverDir = serverNameToDirName(opts.serverName);
|
|
31
|
+
const cacheFile = path.join(dataDir, "cache", serverDir, "tools.json");
|
|
31
32
|
const maxAgeMs = typeof opts.maxAgeMs === "number" ? opts.maxAgeMs : 30 * 60 * 1000; // 30 minutes
|
|
32
33
|
|
|
33
34
|
if (!opts.refresh) {
|
|
@@ -46,6 +47,7 @@ export async function getToolsCached(opts) {
|
|
|
46
47
|
const payload = {
|
|
47
48
|
fetchedAt: new Date().toISOString(),
|
|
48
49
|
serverName: opts.serverName,
|
|
50
|
+
serverDir,
|
|
49
51
|
protocolVersion: opts.client.negotiatedProtocolVersion,
|
|
50
52
|
serverInfo: opts.client.serverInfo,
|
|
51
53
|
capabilities: opts.client.serverCapabilities,
|
|
@@ -17,6 +17,25 @@ export function sha256Hex(s) {
|
|
|
17
17
|
return crypto.createHash("sha256").update(s).digest("hex");
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Convert an arbitrary server name into a safe directory segment.
|
|
22
|
+
* Always sanitizes and appends hash suffix for safety.
|
|
23
|
+
* @param {string} serverName
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function serverNameToDirName(serverName) {
|
|
27
|
+
const raw = String(serverName ?? "");
|
|
28
|
+
const hash = sha256Hex(raw).slice(0, 8);
|
|
29
|
+
const base = raw
|
|
30
|
+
.replace(/[/\\]/g, "_") // block path separators
|
|
31
|
+
.replace(/\.\./g, "_") // block traversal
|
|
32
|
+
.replace(/[^a-zA-Z0-9._-]/g, "_") // whitelist safe chars
|
|
33
|
+
.replace(/^\.+/g, "") // avoid dotfiles
|
|
34
|
+
.slice(0, 40);
|
|
35
|
+
|
|
36
|
+
return `${base || "server"}-${hash}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
20
39
|
/**
|
|
21
40
|
* Convert a tool name (often snake_case or kebab-case) to a safe camelCase identifier.
|
|
22
41
|
* @param {string} toolName
|
|
@@ -37,6 +56,55 @@ export function safeIdentifier(toolName) {
|
|
|
37
56
|
return "_" + camel;
|
|
38
57
|
}
|
|
39
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Build a collision-free safeName -> toolName map.
|
|
61
|
+
* All colliding tools get a hash suffix (not just the "losers").
|
|
62
|
+
* Guarantees deterministic output regardless of input order.
|
|
63
|
+
*
|
|
64
|
+
* @param {string[]} toolNames
|
|
65
|
+
* @returns {Record<string, string>} safeName -> originalToolName
|
|
66
|
+
*/
|
|
67
|
+
export function uniqueSafeToolMap(toolNames) {
|
|
68
|
+
const map = Object.create(null);
|
|
69
|
+
|
|
70
|
+
// Phase 1: Group by base identifier, caching base names
|
|
71
|
+
// Handle undefined/null names with fallback (matches prior behavior)
|
|
72
|
+
const groups = new Map(); // base -> [{name, base}, ...]
|
|
73
|
+
for (const rawName of toolNames) {
|
|
74
|
+
// Matches exact prior semantics: t?.name || "tool"
|
|
75
|
+
// Only null/undefined/empty-string fall back; whitespace-only IS kept
|
|
76
|
+
const toolName = rawName || "tool";
|
|
77
|
+
const base = safeIdentifier(toolName);
|
|
78
|
+
if (!groups.has(base)) groups.set(base, []);
|
|
79
|
+
groups.get(base).push({ name: toolName, base });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Phase 2: Assign safe names (sort colliding groups for determinism)
|
|
83
|
+
for (const [base, tools] of groups) {
|
|
84
|
+
if (tools.length === 1) {
|
|
85
|
+
// No collision: use base name directly
|
|
86
|
+
map[base] = tools[0].name;
|
|
87
|
+
} else {
|
|
88
|
+
// Collision: sort by string comparison for determinism
|
|
89
|
+
// (localeCompare varies by OS/locale; relational operators are consistent)
|
|
90
|
+
tools.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
91
|
+
for (const { name: toolName, base: cachedBase } of tools) {
|
|
92
|
+
const suffix = sha256Hex(toolName).slice(0, 8); // 8 chars = 4B combinations
|
|
93
|
+
let safe = `${cachedBase}_${suffix}`;
|
|
94
|
+
// Guarantee uniqueness even on hash collision (extremely rare)
|
|
95
|
+
let counter = 0;
|
|
96
|
+
while (map[safe] !== undefined) {
|
|
97
|
+
counter++;
|
|
98
|
+
safe = `${cachedBase}_${suffix}_${counter}`;
|
|
99
|
+
}
|
|
100
|
+
map[safe] = toolName;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return map;
|
|
106
|
+
}
|
|
107
|
+
|
|
40
108
|
/**
|
|
41
109
|
* Lightweight CLI args parser (supports: --k=v, --k v, flags, positional args).
|
|
42
110
|
* Returns { _: string[], flags: Record<string, string|boolean> }.
|