@glasstrace/sdk 1.4.0 → 1.5.0

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.
Files changed (43) hide show
  1. package/README.md +56 -0
  2. package/dist/{chunk-JZ475QRH.js → chunk-D3QXU2VM.js} +22 -191
  3. package/dist/chunk-D3QXU2VM.js.map +1 -0
  4. package/dist/{chunk-VQDYXXVS.js → chunk-N3XIVM2U.js} +154 -8
  5. package/dist/chunk-N3XIVM2U.js.map +1 -0
  6. package/dist/{chunk-VJQIFY33.js → chunk-YLY7AGLC.js} +7 -4
  7. package/dist/chunk-YLY7AGLC.js.map +1 -0
  8. package/dist/chunk-ZBQQXVHD.js +208 -0
  9. package/dist/chunk-ZBQQXVHD.js.map +1 -0
  10. package/dist/cli/init.cjs +206 -34
  11. package/dist/cli/init.cjs.map +1 -1
  12. package/dist/cli/init.js +65 -8
  13. package/dist/cli/init.js.map +1 -1
  14. package/dist/cli/mcp-add.cjs +45 -25
  15. package/dist/cli/mcp-add.cjs.map +1 -1
  16. package/dist/cli/mcp-add.js +10 -7
  17. package/dist/cli/mcp-add.js.map +1 -1
  18. package/dist/cli/status.cjs +33 -3
  19. package/dist/cli/status.cjs.map +1 -1
  20. package/dist/cli/status.js +12 -3
  21. package/dist/cli/status.js.map +1 -1
  22. package/dist/cli/uninit.cjs +27 -3
  23. package/dist/cli/uninit.cjs.map +1 -1
  24. package/dist/cli/uninit.d.cts +10 -2
  25. package/dist/cli/uninit.d.ts +10 -2
  26. package/dist/cli/uninit.js +2 -1
  27. package/dist/cli/upgrade-instructions.cjs +440 -0
  28. package/dist/cli/upgrade-instructions.cjs.map +1 -0
  29. package/dist/cli/upgrade-instructions.d.cts +48 -0
  30. package/dist/cli/upgrade-instructions.d.ts +48 -0
  31. package/dist/cli/upgrade-instructions.js +80 -0
  32. package/dist/cli/upgrade-instructions.js.map +1 -0
  33. package/dist/index.cjs +229 -60
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.js +2 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/node-entry.cjs +237 -68
  38. package/dist/node-entry.cjs.map +1 -1
  39. package/dist/node-entry.js +2 -1
  40. package/package.json +1 -1
  41. package/dist/chunk-JZ475QRH.js.map +0 -1
  42. package/dist/chunk-VJQIFY33.js.map +0 -1
  43. package/dist/chunk-VQDYXXVS.js.map +0 -1
@@ -125,8 +125,16 @@ declare function isInitCreatedInstrumentation(content: string): boolean;
125
125
  declare function removeRegisterGlasstrace(content: string): string;
126
126
  /**
127
127
  * Removes content between glasstrace marker comments from a file.
128
- * Supports both HTML markers (`<!-- glasstrace:mcp:start/end -->`) and
129
- * hash markers (`# glasstrace:mcp:start/end`).
128
+ * Supports both legacy unstamped markers (pre-SDK-050) and SDK-050+
129
+ * stamped markers (`<!-- glasstrace:mcp:start v=<sdkVersion> -->` /
130
+ * `# glasstrace:mcp:start v=<sdkVersion>`) for both HTML and hash
131
+ * conventions, by deferring marker recognition to the shared parser
132
+ * in `agent-detection/inject.ts`.
133
+ *
134
+ * Anchoring matches `findMarkerBoundaries` in inject.ts: when multiple
135
+ * start markers appear before the first end marker (e.g. a quoted
136
+ * example marker line followed by the real managed block), the
137
+ * removal window anchors to the MOST RECENT start preceding the end.
130
138
  *
131
139
  * @internal Exported for unit testing only.
132
140
  */
@@ -125,8 +125,16 @@ declare function isInitCreatedInstrumentation(content: string): boolean;
125
125
  declare function removeRegisterGlasstrace(content: string): string;
126
126
  /**
127
127
  * Removes content between glasstrace marker comments from a file.
128
- * Supports both HTML markers (`<!-- glasstrace:mcp:start/end -->`) and
129
- * hash markers (`# glasstrace:mcp:start/end`).
128
+ * Supports both legacy unstamped markers (pre-SDK-050) and SDK-050+
129
+ * stamped markers (`<!-- glasstrace:mcp:start v=<sdkVersion> -->` /
130
+ * `# glasstrace:mcp:start v=<sdkVersion>`) for both HTML and hash
131
+ * conventions, by deferring marker recognition to the shared parser
132
+ * in `agent-detection/inject.ts`.
133
+ *
134
+ * Anchoring matches `findMarkerBoundaries` in inject.ts: when multiple
135
+ * start markers appear before the first end marker (e.g. a quoted
136
+ * example marker line followed by the real managed block), the
137
+ * removal window anchors to the MOST RECENT start preceding the end.
130
138
  *
131
139
  * @internal Exported for unit testing only.
132
140
  */
@@ -12,10 +12,11 @@ import {
12
12
  unwrapCJSExport,
13
13
  unwrapExport,
14
14
  writeShutdownMarker
15
- } from "../chunk-VJQIFY33.js";
15
+ } from "../chunk-YLY7AGLC.js";
16
16
  import "../chunk-MFYOQOD7.js";
17
17
  import "../chunk-4WI7B5FQ.js";
18
18
  import "../chunk-NB7GJE4S.js";
19
+ import "../chunk-ZBQQXVHD.js";
19
20
  import "../chunk-NSBPE2FW.js";
20
21
  export {
21
22
  findMatchingDelimiter,
@@ -0,0 +1,440 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/cli/upgrade-instructions.ts
22
+ var upgrade_instructions_exports = {};
23
+ __export(upgrade_instructions_exports, {
24
+ runUpgradeInstructions: () => runUpgradeInstructions
25
+ });
26
+ module.exports = __toCommonJS(upgrade_instructions_exports);
27
+ var import_node_path3 = require("node:path");
28
+
29
+ // src/mcp-runtime.ts
30
+ var MCP_ENDPOINT = "https://api.glasstrace.dev/mcp";
31
+
32
+ // src/agent-detection/detect.ts
33
+ var import_node_child_process = require("node:child_process");
34
+ var import_promises = require("node:fs/promises");
35
+ var import_node_path = require("node:path");
36
+ var import_node_os = require("node:os");
37
+ var import_node_fs = require("node:fs");
38
+ var AGENT_RULES = [
39
+ {
40
+ name: "claude",
41
+ markers: [".claude", "CLAUDE.md"],
42
+ mcpConfigPath: (dir) => (0, import_node_path.join)(dir, ".mcp.json"),
43
+ infoFilePath: (dir) => (0, import_node_path.join)(dir, "CLAUDE.md"),
44
+ cliBinary: "claude",
45
+ registrationCommand: "npx glasstrace mcp add --agent claude"
46
+ },
47
+ {
48
+ name: "codex",
49
+ markers: ["codex.md", ".codex"],
50
+ mcpConfigPath: (dir) => (0, import_node_path.join)(dir, ".codex", "config.toml"),
51
+ infoFilePath: (dir) => (0, import_node_path.join)(dir, "codex.md"),
52
+ cliBinary: "codex",
53
+ registrationCommand: "npx glasstrace mcp add --agent codex"
54
+ },
55
+ {
56
+ name: "gemini",
57
+ markers: [".gemini"],
58
+ mcpConfigPath: (dir) => (0, import_node_path.join)(dir, ".gemini", "settings.json"),
59
+ infoFilePath: () => null,
60
+ cliBinary: "gemini",
61
+ registrationCommand: "npx glasstrace mcp add --agent gemini"
62
+ },
63
+ {
64
+ name: "cursor",
65
+ markers: [".cursor", ".cursorrules"],
66
+ mcpConfigPath: (dir) => (0, import_node_path.join)(dir, ".cursor", "mcp.json"),
67
+ infoFilePath: (dir) => (0, import_node_path.join)(dir, ".cursorrules"),
68
+ cliBinary: null,
69
+ registrationCommand: "npx glasstrace mcp add --agent cursor"
70
+ },
71
+ {
72
+ name: "windsurf",
73
+ markers: [".windsurfrules", ".windsurf"],
74
+ mcpConfigPath: () => (0, import_node_path.join)((0, import_node_os.homedir)(), ".codeium", "windsurf", "mcp_config.json"),
75
+ infoFilePath: (dir) => (0, import_node_path.join)(dir, ".windsurfrules"),
76
+ cliBinary: null,
77
+ registrationCommand: "npx glasstrace mcp add --agent windsurf"
78
+ }
79
+ ];
80
+ async function pathExists(path, mode = import_node_fs.constants.R_OK) {
81
+ try {
82
+ await (0, import_promises.access)(path, mode);
83
+ return true;
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+ async function findGitRoot(startDir) {
89
+ let current = (0, import_node_path.resolve)(startDir);
90
+ while (true) {
91
+ if (await pathExists((0, import_node_path.join)(current, ".git"), import_node_fs.constants.F_OK)) {
92
+ return current;
93
+ }
94
+ const parent = (0, import_node_path.dirname)(current);
95
+ if (parent === current) {
96
+ break;
97
+ }
98
+ current = parent;
99
+ }
100
+ return (0, import_node_path.resolve)(startDir);
101
+ }
102
+ function isCliAvailable(binary) {
103
+ return new Promise((resolve2) => {
104
+ const command = process.platform === "win32" ? "where" : "which";
105
+ (0, import_node_child_process.execFile)(command, [binary], (error) => {
106
+ resolve2(error === null);
107
+ });
108
+ });
109
+ }
110
+ async function detectAgents(projectRoot) {
111
+ const resolvedRoot = (0, import_node_path.resolve)(projectRoot);
112
+ let rootStat;
113
+ try {
114
+ rootStat = await (0, import_promises.stat)(resolvedRoot);
115
+ } catch (err) {
116
+ const code = err.code;
117
+ throw new Error(
118
+ `projectRoot does not exist: ${resolvedRoot}` + (code ? ` (${code})` : "")
119
+ );
120
+ }
121
+ if (!rootStat.isDirectory()) {
122
+ throw new Error(`projectRoot is not a directory: ${resolvedRoot}`);
123
+ }
124
+ const gitRoot = await findGitRoot(resolvedRoot);
125
+ const searchDirs = [];
126
+ let current = resolvedRoot;
127
+ while (true) {
128
+ searchDirs.push(current);
129
+ if (current === gitRoot) {
130
+ break;
131
+ }
132
+ const parent = (0, import_node_path.dirname)(current);
133
+ if (parent === current) {
134
+ break;
135
+ }
136
+ current = parent;
137
+ }
138
+ const detected = [];
139
+ const seenAgents = /* @__PURE__ */ new Set();
140
+ for (const rule of AGENT_RULES) {
141
+ let foundDir = null;
142
+ for (const dir of searchDirs) {
143
+ let markerFound = false;
144
+ for (const marker of rule.markers) {
145
+ if (await pathExists((0, import_node_path.join)(dir, marker))) {
146
+ markerFound = true;
147
+ break;
148
+ }
149
+ }
150
+ if (markerFound) {
151
+ foundDir = dir;
152
+ break;
153
+ }
154
+ }
155
+ if (foundDir === null) {
156
+ continue;
157
+ }
158
+ if (seenAgents.has(rule.name)) {
159
+ continue;
160
+ }
161
+ seenAgents.add(rule.name);
162
+ let infoFilePath = rule.infoFilePath(foundDir);
163
+ if (infoFilePath !== null && !await pathExists(infoFilePath)) {
164
+ infoFilePath = null;
165
+ }
166
+ const cliAvailable = rule.cliBinary ? await isCliAvailable(rule.cliBinary) : false;
167
+ detected.push({
168
+ name: rule.name,
169
+ mcpConfigPath: rule.mcpConfigPath(foundDir),
170
+ infoFilePath,
171
+ cliAvailable,
172
+ registrationCommand: rule.registrationCommand
173
+ });
174
+ }
175
+ detected.push({
176
+ name: "generic",
177
+ mcpConfigPath: (0, import_node_path.join)(resolvedRoot, ".glasstrace", "mcp.json"),
178
+ infoFilePath: null,
179
+ cliAvailable: false,
180
+ registrationCommand: null
181
+ });
182
+ return detected;
183
+ }
184
+
185
+ // src/agent-detection/configs.ts
186
+ var SDK_VERSION_STAMP_PATTERN = /^[A-Za-z0-9.+-]+$/;
187
+ function htmlMarkers(sdkVersion) {
188
+ return {
189
+ start: `<!-- glasstrace:mcp:start v=${sdkVersion} -->`,
190
+ end: "<!-- glasstrace:mcp:end -->"
191
+ };
192
+ }
193
+ function hashMarkers(sdkVersion) {
194
+ return {
195
+ start: `# glasstrace:mcp:start v=${sdkVersion}`,
196
+ end: "# glasstrace:mcp:end"
197
+ };
198
+ }
199
+ function generateInfoSection(agent, endpoint, sdkVersion) {
200
+ if (!endpoint || endpoint.trim() === "") {
201
+ throw new Error("endpoint must not be empty");
202
+ }
203
+ if (!sdkVersion || sdkVersion.trim() === "") {
204
+ throw new Error("sdkVersion must not be empty");
205
+ }
206
+ if (!SDK_VERSION_STAMP_PATTERN.test(sdkVersion)) {
207
+ throw new Error(
208
+ "sdkVersion must match [A-Za-z0-9.+\\-]+ (semver-shaped, no whitespace, no angle brackets)"
209
+ );
210
+ }
211
+ const content = [
212
+ "",
213
+ "## Glasstrace MCP Integration",
214
+ "",
215
+ `Glasstrace is configured as an MCP server at: ${endpoint}`,
216
+ "",
217
+ "Glasstrace MCP is available when runtime evidence would materially reduce uncertainty. Use it when there is a failing request, stack trace, unclear runtime behavior, race/data-flow symptom, side effect, or performance issue that source inspection alone does not explain. For a current error, `get_latest_error` or `get_error_list` is usually the cheapest orientation call. For a known route/procedure with no exact error, use `find_trace_candidates` and follow returned exact `get_trace` or `get_root_cause` arguments only if the candidates look relevant. Do not call trace tools for trivial source-local fixes. Treat **no candidates** or **no_traces_found** as a scoped retrieval result, not proof the bug is absent.",
218
+ "",
219
+ "Available tools:",
220
+ "- `get_latest_error` - Get the most recent error trace from the current session",
221
+ "- `find_trace_candidates` - First-contact route/procedure/URL candidate selection when you have a route fragment, tRPC procedure, method, status, or rough recent activity window but not the exact trace ID. Returns candidate traces plus suggested `get_trace` / `get_root_cause` follow-up call arguments. Candidate discovery, not root-cause proof.",
222
+ "- `get_error_list` - List recent errors with filtering and pagination",
223
+ "- `get_trace` - Get a specific trace by ID or URL pattern",
224
+ "- `get_root_cause` - Get the root cause analysis for a specific error trace (requires a `traceId` from `get_latest_error`, `get_error_list`, or `get_trace`)",
225
+ "- `get_test_suggestions` - Get test suggestions based on recent errors",
226
+ "- `get_session_timeline` - Get the timeline of all traces in the current session",
227
+ "",
228
+ "To refresh this managed section after a `@glasstrace/sdk` upgrade, run: `npx glasstrace upgrade-instructions`. To reconfigure MCP credentials, run: `npx glasstrace mcp add`.",
229
+ ""
230
+ ].join("\n");
231
+ switch (agent.name) {
232
+ case "claude": {
233
+ const m = htmlMarkers(sdkVersion);
234
+ return `${m.start}
235
+ ${content}${m.end}
236
+ `;
237
+ }
238
+ case "codex": {
239
+ const m = htmlMarkers(sdkVersion);
240
+ return `${m.start}
241
+ ${content}${m.end}
242
+ `;
243
+ }
244
+ case "cursor": {
245
+ const m = hashMarkers(sdkVersion);
246
+ return `${m.start}
247
+ ${content}${m.end}
248
+ `;
249
+ }
250
+ case "gemini":
251
+ case "windsurf":
252
+ case "generic":
253
+ return "";
254
+ default: {
255
+ const _exhaustive = agent.name;
256
+ throw new Error(`Unknown agent: ${_exhaustive}`);
257
+ }
258
+ }
259
+ }
260
+
261
+ // src/agent-detection/inject.ts
262
+ var import_promises2 = require("node:fs/promises");
263
+ var import_node_path2 = require("node:path");
264
+ var HTML_START_RE = /^<!--\s*glasstrace:mcp:start(?:\s+v=([^\s>]+))?\s*-->$/;
265
+ var HTML_END = "<!-- glasstrace:mcp:end -->";
266
+ var HASH_START_RE = /^#\s*glasstrace:mcp:start(?:\s+v=(\S+))?$/;
267
+ var HASH_END = "# glasstrace:mcp:end";
268
+ function parseStartMarkerLine(line) {
269
+ const trimmed = line.trim();
270
+ const html = HTML_START_RE.exec(trimmed);
271
+ if (html !== null) {
272
+ return { kind: "html", stamp: html[1] ?? null };
273
+ }
274
+ const hash = HASH_START_RE.exec(trimmed);
275
+ if (hash !== null) {
276
+ return { kind: "hash", stamp: hash[1] ?? null };
277
+ }
278
+ return null;
279
+ }
280
+ function isEndMarker(line) {
281
+ const trimmed = line.trim();
282
+ return trimmed === HTML_END || trimmed === HASH_END;
283
+ }
284
+ function isPermissionError(err) {
285
+ const code = err.code;
286
+ return code === "EACCES" || code === "EPERM" || code === "EROFS";
287
+ }
288
+ function findMarkerBoundaries(lines) {
289
+ let startIdx = -1;
290
+ for (let i = 0; i < lines.length; i++) {
291
+ if (parseStartMarkerLine(lines[i]) !== null) {
292
+ startIdx = i;
293
+ } else if (startIdx !== -1 && isEndMarker(lines[i])) {
294
+ return { startIdx, endIdx: i };
295
+ }
296
+ }
297
+ return null;
298
+ }
299
+ async function injectInfoSection(agent, content, projectRoot) {
300
+ if (agent.infoFilePath === null) {
301
+ return;
302
+ }
303
+ if (content === "") {
304
+ return;
305
+ }
306
+ const filePath = agent.infoFilePath;
307
+ let existingContent = null;
308
+ try {
309
+ existingContent = await (0, import_promises2.readFile)(filePath, "utf-8");
310
+ } catch (err) {
311
+ const code = err.code;
312
+ if (code !== "ENOENT") {
313
+ if (isPermissionError(err)) {
314
+ process.stderr.write(
315
+ `Warning: cannot read info file ${filePath}: permission denied
316
+ `
317
+ );
318
+ return;
319
+ }
320
+ throw err;
321
+ }
322
+ }
323
+ if (existingContent === null) {
324
+ try {
325
+ await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(filePath), { recursive: true });
326
+ await (0, import_promises2.writeFile)(filePath, content, "utf-8");
327
+ } catch (err) {
328
+ if (isPermissionError(err)) {
329
+ process.stderr.write(
330
+ `Warning: cannot write info file ${filePath}: permission denied
331
+ `
332
+ );
333
+ return;
334
+ }
335
+ throw err;
336
+ }
337
+ return;
338
+ }
339
+ const lines = existingContent.split("\n");
340
+ const boundaries = findMarkerBoundaries(lines);
341
+ let newContent;
342
+ if (boundaries !== null) {
343
+ const before = lines.slice(0, boundaries.startIdx);
344
+ const after = lines.slice(boundaries.endIdx + 1);
345
+ const contentWithoutTrailingNewline = content.endsWith("\n") ? content.slice(0, -1) : content;
346
+ newContent = [...before, contentWithoutTrailingNewline, ...after].join("\n");
347
+ } else {
348
+ const separator = existingContent.endsWith("\n") ? "\n" : "\n\n";
349
+ newContent = existingContent + separator + content;
350
+ }
351
+ try {
352
+ await (0, import_promises2.writeFile)(filePath, newContent, "utf-8");
353
+ } catch (err) {
354
+ if (isPermissionError(err)) {
355
+ process.stderr.write(
356
+ `Warning: cannot write info file ${filePath}: permission denied
357
+ `
358
+ );
359
+ return;
360
+ }
361
+ throw err;
362
+ }
363
+ }
364
+ async function hasManagedSection(filePath) {
365
+ let content;
366
+ try {
367
+ content = await (0, import_promises2.readFile)(filePath, "utf-8");
368
+ } catch (err) {
369
+ const code = err.code;
370
+ if (code === "ENOENT") return false;
371
+ throw err;
372
+ }
373
+ return findMarkerBoundaries(content.split("\n")) !== null;
374
+ }
375
+
376
+ // src/cli/upgrade-instructions.ts
377
+ function formatPathForOutput(filePath, projectRoot) {
378
+ const rel = (0, import_node_path3.relative)(projectRoot, filePath);
379
+ if (rel === "" || rel.startsWith("..") || (0, import_node_path3.isAbsolute)(rel)) {
380
+ return filePath;
381
+ }
382
+ return rel;
383
+ }
384
+ async function runUpgradeInstructions(options) {
385
+ const refreshed = [];
386
+ const skipped = [];
387
+ const warnings = [];
388
+ const errors = [];
389
+ let agents;
390
+ try {
391
+ agents = await detectAgents(options.projectRoot);
392
+ } catch (err) {
393
+ errors.push(
394
+ `Failed to detect agents: ${err instanceof Error ? err.message : String(err)}`
395
+ );
396
+ return { exitCode: 1, refreshed, skipped, warnings, errors };
397
+ }
398
+ const sdkVersion = true ? "1.5.0" : "0.0.0-dev";
399
+ for (const agent of agents) {
400
+ if (agent.infoFilePath === null) {
401
+ continue;
402
+ }
403
+ const displayPath = formatPathForOutput(
404
+ agent.infoFilePath,
405
+ options.projectRoot
406
+ );
407
+ let containsSection;
408
+ try {
409
+ containsSection = await hasManagedSection(agent.infoFilePath);
410
+ } catch (err) {
411
+ warnings.push(
412
+ `Could not inspect ${displayPath}: ${err instanceof Error ? err.message : String(err)}`
413
+ );
414
+ continue;
415
+ }
416
+ if (!containsSection) {
417
+ skipped.push(displayPath);
418
+ continue;
419
+ }
420
+ const content = generateInfoSection(agent, MCP_ENDPOINT, sdkVersion);
421
+ if (content === "") {
422
+ continue;
423
+ }
424
+ try {
425
+ await injectInfoSection(agent, content, options.projectRoot);
426
+ refreshed.push(displayPath);
427
+ } catch (err) {
428
+ errors.push(
429
+ `Failed to refresh ${displayPath}: ${err instanceof Error ? err.message : String(err)}`
430
+ );
431
+ }
432
+ }
433
+ const exitCode = errors.length === 0 ? 0 : 1;
434
+ return { exitCode, refreshed, skipped, warnings, errors };
435
+ }
436
+ // Annotate the CommonJS export names for ESM import in node:
437
+ 0 && (module.exports = {
438
+ runUpgradeInstructions
439
+ });
440
+ //# sourceMappingURL=upgrade-instructions.cjs.map