droid-mode 0.0.10 → 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.10",
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, serverNameToDirName } 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) {
@@ -104,11 +104,9 @@ export function hydrateTools(opts) {
104
104
  if (t) selected.push(t);
105
105
  }
106
106
 
107
- const toolmap = {};
108
- for (const t of selected) {
109
- const safe = safeIdentifier(t?.name || "tool");
110
- toolmap[safe] = t?.name || safe;
111
- }
107
+ // Build collision-free toolmap using shared function
108
+ const selectedNames = selected.map(t => t?.name || "tool");
109
+ const toolmap = uniqueSafeToolMap(selectedNames);
112
110
 
113
111
  writeJson(path.join(baseOut, "tools.json"), selected);
114
112
  writeJson(path.join(baseOut, "toolmap.json"), toolmap);
@@ -117,15 +115,22 @@ export function hydrateTools(opts) {
117
115
  const toolmapModule = `// Auto-generated by droid-mode.\nexport default ${JSON.stringify(toolmap, null, 2)};\n`;
118
116
  fs.writeFileSync(path.join(baseOut, "toolmap.mjs"), toolmapModule, "utf-8");
119
117
 
120
- // types.d.ts
118
+ // types.d.ts - use toolmap keys (already collision-free)
121
119
  const typeLines = [
122
120
  `// Auto-generated by droid-mode (best-effort).`,
123
121
  `// This file exists to improve IDE autocomplete; it is NOT a contract.`,
124
122
  ``,
125
123
  ];
126
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
+
127
131
  for (const t of selected) {
128
- const safe = safeIdentifier(t?.name || "tool");
132
+ const toolName = t?.name || "tool";
133
+ const safe = nameToSafe[toolName] || safeIdentifier(toolName);
129
134
  const pascal = toPascalCase(safe);
130
135
  const inSchema = t?.inputSchema || t?.parameters || null;
131
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, serverNameToDirName } 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
 
@@ -56,6 +56,55 @@ export function safeIdentifier(toolName) {
56
56
  return "_" + camel;
57
57
  }
58
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
+
59
108
  /**
60
109
  * Lightweight CLI args parser (supports: --k=v, --k v, flags, positional args).
61
110
  * Returns { _: string[], flags: Record<string, string|boolean> }.