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
  {
2
2
  "name": "droid-mode",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "Progressive Code-Mode MCP integration for Factory.ai Droid - access MCP tools without context bloat",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
@@ -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", opts.serverName, ts);
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
- const toolmap = {};
107
- for (const t of selected) {
108
- const safe = safeIdentifier(t?.name || "tool");
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 safe = safeIdentifier(t?.name || "tool");
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, safeIdentifier, ensureDir, getDroidModeDataDir, writeJson } from "./util.mjs";
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
- // safe-name wrapper map
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 outDir = path.join(dataDir, "runs", opts.serverName, ts);
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 cacheFile = path.join(dataDir, "cache", opts.serverName, "tools.json");
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> }.