mantisai-cli 2.0.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.
@@ -0,0 +1,150 @@
1
+ import { execSync, spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ import { loadConfig } from './config.js';
8
+ import { syncMcpConfigs } from './mcp-config.js';
9
+
10
+ const MARKETPLACE = 'mantis-plugins';
11
+ const PLUGIN_ID = `mantis@${MARKETPLACE}`;
12
+ const MARKETPLACE_REPO = 'KellisLab/mantis-cli';
13
+ const NPM_PACKAGE = 'mantisai-cli';
14
+ const LEGACY_NPM_PACKAGES = ['mantis-cli', 'mantis-claude-code'];
15
+
16
+ export function resolvePluginRoot() {
17
+ try {
18
+ const root = execSync('npm root -g', { encoding: 'utf8', windowsHide: true }).trim();
19
+ for (const candidate of [NPM_PACKAGE, ...LEGACY_NPM_PACKAGES]) {
20
+ const globalPkg = path.join(root, candidate);
21
+ if (fs.existsSync(path.join(globalPkg, '.claude-plugin', 'plugin.json'))) {
22
+ return path.normalize(globalPkg);
23
+ }
24
+ }
25
+ } catch {
26
+ /* not installed globally */
27
+ }
28
+ const here = path.dirname(fileURLToPath(import.meta.url));
29
+ return path.normalize(path.resolve(here, '..'));
30
+ }
31
+
32
+ function settingsPath() {
33
+ return path.join(os.homedir(), '.claude', 'settings.json');
34
+ }
35
+
36
+ function installedPluginsPath() {
37
+ return path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
38
+ }
39
+
40
+ function readSettings() {
41
+ const file = settingsPath();
42
+ if (!fs.existsSync(file)) return {};
43
+ try {
44
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
45
+ } catch {
46
+ return {};
47
+ }
48
+ }
49
+
50
+ function writeSettings(settings) {
51
+ const file = settingsPath();
52
+ fs.mkdirSync(path.dirname(file), { recursive: true });
53
+ fs.writeFileSync(file, `${JSON.stringify(settings, null, 2)}\n`);
54
+ }
55
+
56
+ function stripBrokenInlinePlugins(settings) {
57
+ const bad = (v) =>
58
+ typeof v === 'string' &&
59
+ (v.includes('$(npm') || v.includes('mantis-claude-code-plugin') || v.includes('mantis-cli-plugin'));
60
+ if (Array.isArray(settings.plugins)) {
61
+ settings.plugins = settings.plugins.filter((p) => {
62
+ const paths = [p?.path, p?.commands, p?.source?.path].filter(Boolean);
63
+ return !paths.some(bad);
64
+ });
65
+ if (!settings.plugins.length) delete settings.plugins;
66
+ }
67
+ }
68
+
69
+ export function isPluginInstalled() {
70
+ try {
71
+ const data = JSON.parse(fs.readFileSync(installedPluginsPath(), 'utf8'));
72
+ const entries = data.plugins?.[PLUGIN_ID];
73
+ if (!Array.isArray(entries) || !entries.length) return false;
74
+ const installPath = entries[0]?.installPath;
75
+ if (!installPath || !fs.existsSync(installPath)) return false;
76
+ return !fs.existsSync(path.join(installPath, '.orphaned_at'));
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ function mergeMarketplaceIntoSettings() {
83
+ const settings = readSettings();
84
+ stripBrokenInlinePlugins(settings);
85
+ settings.extraKnownMarketplaces = settings.extraKnownMarketplaces || {};
86
+ settings.extraKnownMarketplaces[MARKETPLACE] = {
87
+ source: { source: 'github', repo: MARKETPLACE_REPO },
88
+ };
89
+ writeSettings(settings);
90
+ }
91
+
92
+ const BENIGN = [
93
+ /already enabled/i,
94
+ /already installed/i,
95
+ /already on disk/i,
96
+ /no updates/i,
97
+ /is already enabled/i,
98
+ ];
99
+
100
+ function quoteCmdArg(arg) {
101
+ return `"${String(arg).replace(/"/g, '\\"')}"`;
102
+ }
103
+
104
+ function runClaude(argv, { optional = false } = {}) {
105
+ const opts = { encoding: 'utf8', windowsHide: true };
106
+ const r = process.platform === 'win32'
107
+ ? spawnSync(['claude', ...argv.map(quoteCmdArg)].join(' '), { ...opts, shell: true })
108
+ : spawnSync('claude', argv, opts);
109
+ if (r.error) {
110
+ const msg =
111
+ r.error.code === 'ENOENT'
112
+ ? 'claude not found on PATH — install Claude Code CLI first'
113
+ : r.error.message;
114
+ if (optional) return null;
115
+ throw new Error(msg);
116
+ }
117
+ const out = `${r.stdout || ''}${r.stderr || ''}`.trim();
118
+ if (r.status === 0 || BENIGN.some((re) => re.test(out))) return out;
119
+ if (optional) return null;
120
+ throw new Error(out || `claude ${argv.join(' ')} failed (exit ${r.status})`);
121
+ }
122
+
123
+ function installPluginScopes() {
124
+ for (const scope of ['user', 'local']) {
125
+ if (isPluginInstalled()) return scope;
126
+ runClaude(['plugin', 'install', PLUGIN_ID, '--scope', scope], { optional: true });
127
+ }
128
+ return isPluginInstalled() ? 'user' : null;
129
+ }
130
+
131
+ export function installClaudePlugin() {
132
+ const pluginRoot = resolvePluginRoot();
133
+ mergeMarketplaceIntoSettings();
134
+
135
+ runClaude(['plugin', 'marketplace', 'add', MARKETPLACE_REPO, '--scope', 'user'], { optional: true });
136
+ const scope = installPluginScopes();
137
+ if (!scope) {
138
+ throw new Error(
139
+ 'Plugin not registered after install. Run: claude plugin install mantis@mantis-plugins',
140
+ );
141
+ }
142
+ runClaude(['plugin', 'update', PLUGIN_ID, '--scope', scope], { optional: true });
143
+ runClaude(['plugin', 'enable', PLUGIN_ID, '--scope', scope], { optional: true });
144
+ const settings = readSettings();
145
+ settings.enabledPlugins = settings.enabledPlugins || {};
146
+ settings.enabledPlugins[PLUGIN_ID] = true;
147
+ writeSettings(settings);
148
+ syncMcpConfigs(loadConfig());
149
+ return { ok: true, method: 'cli', pluginRoot, scope };
150
+ }
@@ -0,0 +1,115 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import fg from 'fast-glob';
4
+ import { stringify } from 'csv-stringify/sync';
5
+
6
+ const DEFAULT_IGNORES = [
7
+ '**/.git/**',
8
+ '**/.next/**',
9
+ '**/.turbo/**',
10
+ '**/.venv/**',
11
+ '**/__pycache__/**',
12
+ '**/build/**',
13
+ '**/coverage/**',
14
+ '**/dist/**',
15
+ '**/node_modules/**',
16
+ '**/vendor/**',
17
+ '**/*.lock',
18
+ '**/package-lock.json',
19
+ '**/pnpm-lock.yaml',
20
+ '**/yarn.lock',
21
+ ];
22
+
23
+ const EXT_LANGUAGE = {
24
+ '.css': 'css',
25
+ '.go': 'go',
26
+ '.html': 'html',
27
+ '.java': 'java',
28
+ '.js': 'javascript',
29
+ '.jsx': 'javascript',
30
+ '.json': 'json',
31
+ '.md': 'markdown',
32
+ '.py': 'python',
33
+ '.rs': 'rust',
34
+ '.scss': 'scss',
35
+ '.sql': 'sql',
36
+ '.ts': 'typescript',
37
+ '.tsx': 'typescript',
38
+ '.vue': 'vue',
39
+ '.yaml': 'yaml',
40
+ '.yml': 'yaml',
41
+ };
42
+
43
+ const SOURCE_EXTENSIONS = Object.keys(EXT_LANGUAGE);
44
+
45
+ function inferKind(rel) {
46
+ const base = path.basename(rel).toLowerCase();
47
+ if (base.includes('test') || base.includes('spec')) return 'test';
48
+ if (rel.includes('/components/') || rel.includes('\\components\\')) return 'component';
49
+ if (rel.includes('/pages/') || rel.includes('\\pages\\') || rel.includes('/app/')) return 'route';
50
+ if (rel.includes('/api/') || rel.includes('\\api\\')) return 'api';
51
+ if (rel.includes('/hooks/') || rel.includes('\\hooks\\')) return 'hook';
52
+ if (rel.includes('/utils/') || rel.includes('\\utils\\') || rel.includes('/lib/')) return 'utility';
53
+ return 'source';
54
+ }
55
+
56
+ function importsFrom(content) {
57
+ const imports = new Set();
58
+ const patterns = [
59
+ /^\s*import\s+.*?\s+from\s+['"]([^'"]+)['"]/gm,
60
+ /^\s*import\s+['"]([^'"]+)['"]/gm,
61
+ /^\s*const\s+.*?=\s+require\(['"]([^'"]+)['"]\)/gm,
62
+ /^\s*from\s+([\w.]+)\s+import\s+/gm,
63
+ ];
64
+ for (const re of patterns) {
65
+ for (const match of content.matchAll(re)) imports.add(match[1]);
66
+ }
67
+ return [...imports].slice(0, 40).join(', ');
68
+ }
69
+
70
+ function summarize(content) {
71
+ const lines = content.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
72
+ const comment = lines.find((l) => /^\/\/|^#|^\/\*|^\*|^<!--/.test(l));
73
+ return (comment || lines[0] || '').replace(/^\/\/|^#|^\/\*+|^\*|<!--|-->/g, '').trim().slice(0, 240);
74
+ }
75
+
76
+ function readText(file, maxChars) {
77
+ const raw = fs.readFileSync(file, 'utf8');
78
+ return raw.length > maxChars ? `${raw.slice(0, maxChars)}\n\n[truncated]` : raw;
79
+ }
80
+
81
+ export async function createCodebaseCsv(rootDir, outFile, { maxChars = 12000, include } = {}) {
82
+ const root = path.resolve(rootDir || process.cwd());
83
+ const patterns = include?.length ? include : SOURCE_EXTENSIONS.map((ext) => `**/*${ext}`);
84
+ const files = await fg(patterns, {
85
+ cwd: root,
86
+ absolute: true,
87
+ dot: false,
88
+ ignore: DEFAULT_IGNORES,
89
+ onlyFiles: true,
90
+ });
91
+
92
+ const rows = files.sort().map((file) => {
93
+ const rel = path.relative(root, file).replace(/\\/g, '/');
94
+ const ext = path.extname(file).toLowerCase();
95
+ const content = readText(file, maxChars);
96
+ const stat = fs.statSync(file);
97
+ return {
98
+ path: rel,
99
+ file_name: path.basename(file),
100
+ extension: ext.replace(/^\./, ''),
101
+ language: EXT_LANGUAGE[ext] || ext.replace(/^\./, ''),
102
+ kind: inferKind(rel),
103
+ loc: content.split(/\r?\n/).length,
104
+ bytes: stat.size,
105
+ imports: importsFrom(content),
106
+ summary: summarize(content),
107
+ content,
108
+ };
109
+ });
110
+
111
+ if (!rows.length) throw new Error(`No source files found in ${root}`);
112
+ fs.mkdirSync(path.dirname(path.resolve(outFile)), { recursive: true });
113
+ fs.writeFileSync(outFile, stringify(rows, { header: true }));
114
+ return { root, outFile: path.resolve(outFile), count: rows.length };
115
+ }
package/lib/config.js ADDED
@@ -0,0 +1,65 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+
5
+ const CONFIG_NAME = 'config.json';
6
+ export const DEFAULT_API_BASE = 'https://kellis-h200-1.csail.mit.edu';
7
+ export const DEVELOPER_PORTAL_URL = 'https://mantis.csail.mit.edu/developer/#keys';
8
+
9
+ export function canonicalConfigPath() {
10
+ return path.join(os.homedir(), '.mantis', 'claude-code', CONFIG_NAME);
11
+ }
12
+
13
+ function pluginDataConfigPath() {
14
+ if (!process.env.CLAUDE_PLUGIN_DATA) return null;
15
+ return path.join(process.env.CLAUDE_PLUGIN_DATA, CONFIG_NAME);
16
+ }
17
+
18
+ /** Paths that may hold config (canonical first). */
19
+ export function allConfigPaths() {
20
+ const paths = [canonicalConfigPath()];
21
+ const plugin = pluginDataConfigPath();
22
+ if (plugin) paths.push(plugin);
23
+ return [...new Set(paths)];
24
+ }
25
+
26
+ export function configPath() {
27
+ return canonicalConfigPath();
28
+ }
29
+
30
+ export function loadConfig() {
31
+ const canonical = canonicalConfigPath();
32
+ if (fs.existsSync(canonical)) {
33
+ try {
34
+ return JSON.parse(fs.readFileSync(canonical, 'utf8'));
35
+ } catch {
36
+ /* fall through */
37
+ }
38
+ }
39
+ const plugin = pluginDataConfigPath();
40
+ if (plugin && fs.existsSync(plugin)) {
41
+ try {
42
+ return JSON.parse(fs.readFileSync(plugin, 'utf8'));
43
+ } catch {
44
+ /* empty */
45
+ }
46
+ }
47
+ return {};
48
+ }
49
+
50
+ export function saveConfig(cfg) {
51
+ const body = JSON.stringify(cfg, null, 2);
52
+ for (const file of allConfigPaths()) {
53
+ fs.mkdirSync(path.dirname(file), { recursive: true });
54
+ fs.writeFileSync(file, body);
55
+ }
56
+ }
57
+
58
+ export function normalizeBaseUrl(url) {
59
+ return String(url || '').trim().replace(/\/+$/, '');
60
+ }
61
+
62
+ export function mcpUrl(cfg) {
63
+ const base = normalizeBaseUrl(cfg.apiBaseUrl || process.env.MANTIS_API_URL || DEFAULT_API_BASE);
64
+ return `${base}/mcp_integrated/`;
65
+ }
package/lib/csv.js ADDED
@@ -0,0 +1,10 @@
1
+ import fs from 'node:fs';
2
+ import { parse } from 'csv-parse/sync';
3
+
4
+ export function readCsvHeaders(file) {
5
+ const input = fs.readFileSync(file, 'utf8');
6
+ const rows = parse(input, { to_line: 1, relax_quotes: true });
7
+ const headers = rows[0] || [];
8
+ if (!headers.length) throw new Error('CSV has no header row.');
9
+ return headers.map((h) => String(h).trim()).filter(Boolean);
10
+ }
package/lib/fetch.js ADDED
@@ -0,0 +1,36 @@
1
+ import { createSpaceState, listSpaceStates, listSpaces } from './api.js';
2
+
3
+ const PAGE = 100;
4
+
5
+ async function fetchPaginated(fetchPage) {
6
+ const all = [];
7
+ let offset = 0;
8
+ for (;;) {
9
+ const { items, total } = await fetchPage(PAGE, offset);
10
+ const batch = items || [];
11
+ all.push(...batch);
12
+ if (batch.length < PAGE) break;
13
+ if (Number.isFinite(total) && all.length >= total) break;
14
+ offset += PAGE;
15
+ }
16
+ return all;
17
+ }
18
+
19
+ export async function fetchAccessibleSpaces(baseUrl, apiKey) {
20
+ return fetchPaginated(async (limit, offset) => {
21
+ const page = await listSpaces(baseUrl, apiKey, { scope: 'accessible', limit, offset });
22
+ return { items: page.spaces, total: page.total };
23
+ });
24
+ }
25
+
26
+ /** @deprecated use fetchAccessibleSpaces */
27
+ export const fetchOwnedSpaces = fetchAccessibleSpaces;
28
+
29
+ export async function fetchThreads(baseUrl, apiKey, spaceId) {
30
+ return fetchPaginated(async (limit, offset) => {
31
+ const page = await listSpaceStates(baseUrl, apiKey, spaceId, { limit, offset });
32
+ return { items: page.space_states, total: page.total };
33
+ });
34
+ }
35
+
36
+ export { createSpaceState };
package/lib/fields.js ADDED
@@ -0,0 +1,61 @@
1
+ export const FIELD_KEYS = [
2
+ 'title',
3
+ 'semantic',
4
+ 'numeric',
5
+ 'categoric',
6
+ 'date',
7
+ 'links',
8
+ 'custom_model',
9
+ 'connection',
10
+ 'delete',
11
+ ];
12
+
13
+ function splitColumns(value) {
14
+ if (!value) return [];
15
+ if (Array.isArray(value)) return value.flatMap(splitColumns);
16
+ return String(value).split(',').map((v) => v.trim()).filter(Boolean);
17
+ }
18
+
19
+ export function columnSet(value) {
20
+ return new Set(splitColumns(value));
21
+ }
22
+
23
+ export function inferFieldTypes(headers, opts = {}) {
24
+ const title = columnSet(opts.titleColumn || opts.titleColumns);
25
+ const semantic = columnSet(opts.semanticColumn || opts.semanticColumns);
26
+ const numeric = columnSet(opts.numericColumn || opts.numericColumns);
27
+ const categoric = columnSet(opts.categoricColumn || opts.categoricColumns);
28
+ const date = columnSet(opts.dateColumn || opts.dateColumns);
29
+ const links = columnSet(opts.linksColumn || opts.linksColumns);
30
+ const deleted = columnSet(opts.deleteColumn || opts.deleteColumns);
31
+
32
+ if (!title.size) {
33
+ const candidate = headers.find((h) => ['title', 'name', 'path', 'file'].includes(h.toLowerCase()));
34
+ if (candidate) title.add(candidate);
35
+ }
36
+ if (!semantic.size) {
37
+ const candidates = headers.filter((h) =>
38
+ ['content', 'summary', 'description', 'text', 'body'].includes(h.toLowerCase()),
39
+ );
40
+ for (const h of candidates.length ? candidates : headers.filter((h) => !title.has(h))) semantic.add(h);
41
+ }
42
+
43
+ return headers.map((h) => ({
44
+ title: title.has(h),
45
+ semantic: semantic.has(h),
46
+ numeric: numeric.has(h),
47
+ categoric: categoric.has(h),
48
+ date: date.has(h),
49
+ links: links.has(h),
50
+ custom_model: false,
51
+ connection: false,
52
+ delete: deleted.has(h),
53
+ }));
54
+ }
55
+
56
+ export function fieldSummary(headers, dataTypes) {
57
+ return headers.map((h, i) => {
58
+ const types = FIELD_KEYS.filter((k) => dataTypes[i]?.[k]);
59
+ return `${h}: ${types.length ? types.join(', ') : 'unused'}`;
60
+ });
61
+ }
@@ -0,0 +1,55 @@
1
+ import { loadConfig } from './config.js';
2
+ import { searchSpaces } from './spaces.js';
3
+ import { fetchThreads } from './fetch.js';
4
+
5
+ export function filterItems(items, query, keys) {
6
+ const terms = (query || '').trim().toLowerCase().split(/\s+/).filter(Boolean);
7
+ if (!terms.length) return items;
8
+ return items.filter((item) => {
9
+ const hay = keys.map((k) => String(item[k] ?? '')).join(' ').toLowerCase();
10
+ return terms.every((t) => hay.includes(t));
11
+ });
12
+ }
13
+
14
+ export async function spacesForPrompt(filter = '', { limit = 4, offset = 0 } = {}) {
15
+ const cfg = loadConfig();
16
+ if (!cfg.apiKey || !cfg.apiBaseUrl) {
17
+ throw new Error('Run mantis setup first (API key + URL).');
18
+ }
19
+ const page = await searchSpaces(cfg.apiBaseUrl, cfg.apiKey, {
20
+ q: filter,
21
+ limit,
22
+ offset,
23
+ });
24
+ return {
25
+ spaces: page.spaces || [],
26
+ total: page.total,
27
+ offset,
28
+ limit,
29
+ hasMore: offset + limit < page.total,
30
+ filter,
31
+ };
32
+ }
33
+
34
+ export async function threadsForPrompt(filter = '', { limit = 4, offset = 0 } = {}) {
35
+ const cfg = loadConfig();
36
+ if (!cfg.apiKey || !cfg.apiBaseUrl) throw new Error('Run mantis setup first.');
37
+ if (!cfg.spaceId) throw new Error('Pick a space first (/mantis:space).');
38
+
39
+ const all = filterItems(
40
+ await fetchThreads(cfg.apiBaseUrl, cfg.apiKey, cfg.spaceId),
41
+ filter,
42
+ ['name', 'id'],
43
+ );
44
+ const page = all.slice(offset, offset + limit);
45
+ return {
46
+ threads: page,
47
+ total: all.length,
48
+ offset,
49
+ limit,
50
+ hasMore: offset + limit < all.length,
51
+ spaceId: cfg.spaceId,
52
+ spaceName: cfg.spaceName,
53
+ filter,
54
+ };
55
+ }
@@ -0,0 +1,128 @@
1
+ import path from 'node:path';
2
+
3
+ import { createMapInSpace, createSpace, createSpaceState } from './api.js';
4
+ import { loadConfig, normalizeBaseUrl, saveConfig } from './config.js';
5
+ import { readCsvHeaders } from './csv.js';
6
+ import { fieldSummary, inferFieldTypes } from './fields.js';
7
+ import { fetchSpaceById, searchSpaces } from './spaces.js';
8
+ import { syncMcpConfigs } from './mcp-config.js';
9
+ import { info, promptConfirm, promptInput, promptSelect, success } from './ui.js';
10
+
11
+ function requireAuth() {
12
+ const cfg = loadConfig();
13
+ if (!cfg.apiKey || !cfg.apiBaseUrl) {
14
+ throw new Error('Run mantis setup first (API key + URL).');
15
+ }
16
+ return cfg;
17
+ }
18
+
19
+ function boolOption(value, fallback = false) {
20
+ if (value == null) return fallback;
21
+ if (typeof value === 'boolean') return value;
22
+ return ['1', 'true', 'yes', 'public'].includes(String(value).toLowerCase());
23
+ }
24
+
25
+ function spaceUrl(baseUrl, spaceId) {
26
+ const root = normalizeBaseUrl(baseUrl).replace(/\/api\/?$/, '');
27
+ return `${root}/space/${spaceId}`;
28
+ }
29
+
30
+ async function chooseExistingSpace(cfg, filter = '') {
31
+ const page = await searchSpaces(cfg.apiBaseUrl, cfg.apiKey, { q: filter, limit: 12 });
32
+ if (!page.spaces?.length) throw new Error('No spaces found.');
33
+ return promptSelect('Which space should receive this map?', page.spaces.map((s) => ({
34
+ name: `${s.name} · ${s.map_count ?? 0} map(s) · ${s.role || 'space'}`,
35
+ value: s,
36
+ description: s.id,
37
+ })));
38
+ }
39
+
40
+ async function resolveSpaceTarget(cfg, opts) {
41
+ let mode = opts.spaceMode;
42
+ if (!mode) {
43
+ mode = await promptSelect('Where should this map go?', [
44
+ { name: 'Create a new space', value: 'new' },
45
+ { name: 'Add to an existing space', value: 'existing' },
46
+ ]);
47
+ }
48
+
49
+ if (mode === 'existing') {
50
+ if (opts.spaceId) {
51
+ const space = opts.spaceName ? null : await fetchSpaceById(cfg.apiBaseUrl, cfg.apiKey, opts.spaceId);
52
+ return { spaceId: opts.spaceId, spaceName: opts.spaceName || space?.name };
53
+ }
54
+ const picked = await chooseExistingSpace(cfg, opts.spaceSearch || '');
55
+ return { spaceId: picked.id, spaceName: picked.name };
56
+ }
57
+
58
+ const spaceName = opts.spaceName || await promptInput('New space name', {
59
+ default: path.basename(opts.file, path.extname(opts.file)) || 'Claude Code Map',
60
+ });
61
+ const isPublic = opts.public != null ? boolOption(opts.public) : await promptConfirm('Make this space public?', { default: false });
62
+ return { spaceName, isPublic };
63
+ }
64
+
65
+ export async function createMapFlow(file, opts = {}) {
66
+ const cfg = requireAuth();
67
+ const mapName = opts.mapName || await promptInput('Map name', {
68
+ default: path.basename(file, path.extname(file)),
69
+ });
70
+ const headers = readCsvHeaders(file);
71
+ const dataTypes = opts.dataTypes ? JSON.parse(opts.dataTypes) : inferFieldTypes(headers, opts);
72
+ info(`Fields: ${fieldSummary(headers, dataTypes).join(' | ')}`);
73
+
74
+ const target = await resolveSpaceTarget(cfg, { ...opts, file });
75
+
76
+ let spaceId = target.spaceId;
77
+ let spaceName = target.spaceName;
78
+ if (!spaceId) {
79
+ const space = await createSpace(cfg.apiBaseUrl, cfg.apiKey, {
80
+ name: target.spaceName,
81
+ isPublic: target.isPublic,
82
+ });
83
+ spaceId = space.id;
84
+ spaceName = space.name;
85
+ info(`Created space: ${spaceName} (${spaceId})`);
86
+ }
87
+
88
+ const result = await createMapInSpace(cfg.apiBaseUrl, cfg.apiKey, spaceId, {
89
+ file,
90
+ mapName,
91
+ dataTypes,
92
+ selectedFields: opts.selectedFields,
93
+ fieldWeights: opts.fieldWeights,
94
+ });
95
+
96
+ const primaryMapId = result.map_id || result.base_map_id || result.map_ids?.[0];
97
+ target.spaceName = spaceName;
98
+ success('Map creation started');
99
+ info(`Space: ${target.spaceName || spaceId} (${spaceId})`);
100
+ info(`Map: ${mapName}${primaryMapId ? ` (${primaryMapId})` : ''}`);
101
+ if (spaceId) info(`Link: ${spaceUrl(cfg.apiBaseUrl, spaceId)}`);
102
+
103
+ const activate = opts.activate != null ? boolOption(opts.activate) : await promptConfirm('Set this as the active Claude Code Mantis space?', { default: true });
104
+ let thread = null;
105
+ if (activate) {
106
+ const threadName = opts.threadName || await promptInput('Thread name', { default: `${mapName} Exploration` });
107
+ thread = await createSpaceState(cfg.apiBaseUrl, cfg.apiKey, spaceId, threadName);
108
+ const next = {
109
+ ...cfg,
110
+ spaceId,
111
+ spaceName: target.spaceName || mapName,
112
+ spaceStateId: thread.id,
113
+ spaceStateName: thread.name,
114
+ };
115
+ saveConfig(next);
116
+ syncMcpConfigs(next);
117
+ success(`Active thread: ${thread.name}`);
118
+ info('Run /reload-plugins in Claude Code before using Mantis MCP tools.');
119
+ }
120
+
121
+ return {
122
+ ...result,
123
+ space_id: spaceId,
124
+ map_id: primaryMapId,
125
+ space_url: spaceId ? spaceUrl(cfg.apiBaseUrl, spaceId) : undefined,
126
+ thread,
127
+ };
128
+ }
@@ -0,0 +1,50 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ import { mcpUrl } from './config.js';
7
+
8
+ const MARKETPLACE = 'mantis-plugins';
9
+ const PACKAGE = 'mantisai-cli';
10
+
11
+ function existingPackageRoot(root) {
12
+ return root && fs.existsSync(path.join(root, 'package.json')) ? path.normalize(root) : null;
13
+ }
14
+
15
+ function isInstallRoot(root) {
16
+ const p = root.toLowerCase();
17
+ return p.includes(`${path.sep}.claude${path.sep}plugins${path.sep}`) ||
18
+ p.endsWith(`${path.sep}node_modules${path.sep}${PACKAGE}`);
19
+ }
20
+
21
+ function cacheRoots() {
22
+ const base = path.join(os.homedir(), '.claude', 'plugins', 'cache', MARKETPLACE, 'mantis');
23
+ if (!fs.existsSync(base)) return [];
24
+ return fs.readdirSync(base).map((v) => path.join(base, v));
25
+ }
26
+
27
+ export function pluginRoots() {
28
+ const here = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
29
+ const home = os.homedir();
30
+ return [
31
+ here,
32
+ path.join(home, '.claude', 'plugins', 'marketplaces', MARKETPLACE),
33
+ path.join(home, '.claude', 'plugins', 'npm-cache', 'node_modules', PACKAGE),
34
+ path.join(home, 'AppData', 'Roaming', 'npm', 'node_modules', PACKAGE),
35
+ ...cacheRoots(),
36
+ ].map(existingPackageRoot).filter(Boolean).filter(isInstallRoot).filter((v, i, a) => a.indexOf(v) === i);
37
+ }
38
+
39
+ export function mcpConfig(cfg) {
40
+ const server = { type: 'http', url: mcpUrl(cfg) };
41
+ if (cfg.spaceStateId) server.headers = { 'X-Space-State-ID': String(cfg.spaceStateId) };
42
+ return { mcpServers: { mantis: server } };
43
+ }
44
+
45
+ export function syncMcpConfigs(cfg) {
46
+ const body = `${JSON.stringify(mcpConfig(cfg), null, 2)}\n`;
47
+ for (const root of pluginRoots()) {
48
+ fs.writeFileSync(path.join(root, '.mcp.json'), body);
49
+ }
50
+ }