Skip to content

Commit 763eb40

Browse files
committed
Search spaces on demand and accept pasted space links
Adds API q/space_id filters, link paste in mantis-setup, and mantis-resolve-space for Claude slash commands. Stops loading the full space catalog up front.
1 parent 189062a commit 763eb40

11 files changed

Lines changed: 173 additions & 53 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
{
88
"name": "mantis",
99
"description": "Mantis MCP: spaces, threads, and map context for Claude Code",
10-
"version": "1.0.7",
10+
"version": "1.0.8",
1111
"source": "./"
1212
}
1313
]

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "mantis",
33
"description": "Connect Claude Code to Mantis MCP — explore maps, clusters, and notebooks in your spaces",
4-
"version": "1.0.7",
4+
"version": "1.0.8",
55
"author": {
66
"name": "MIT Mantis"
77
},

bin/mantis-resolve-space.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env node
2+
import { loadConfig } from '../lib/config.js';
3+
import { resolveSpaceFromInput } from '../lib/spaces.js';
4+
5+
const input = process.argv.slice(2).join(' ').trim();
6+
if (!input) {
7+
console.error(JSON.stringify({ error: 'Provide a space link or UUID' }));
8+
process.exit(1);
9+
}
10+
11+
try {
12+
const cfg = loadConfig();
13+
if (!cfg.apiKey || !cfg.apiBaseUrl) {
14+
throw new Error('Run mantis-setup first (API key + URL).');
15+
}
16+
const space = await resolveSpaceFromInput(cfg.apiBaseUrl, cfg.apiKey, input);
17+
if (!space) {
18+
console.error(JSON.stringify({ error: 'Space not found or no access' }));
19+
process.exit(1);
20+
}
21+
process.stdout.write(JSON.stringify({ space, threadCleared: true }));
22+
} catch (e) {
23+
console.error(JSON.stringify({ error: e.message }));
24+
process.exit(1);
25+
}

bin/mantis-setup.js

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
#!/usr/bin/env node
2-
import { fetchAccessibleSpaces } from '../lib/fetch.js';
32
import { DEFAULT_API_BASE, loadConfig, normalizeBaseUrl, saveConfig } from '../lib/config.js';
43
import { pickSpace, pickThread } from '../lib/picker.js';
54
import { installClaudePlugin } from '../lib/claude-plugin.js';
@@ -17,21 +16,9 @@ async function main() {
1716
const apiKey = (await promptSecret('API key', { default: prev.apiKey }))?.trim();
1817
if (!apiKey) die('API key is required.');
1918

20-
process.stdout.write(' \x1b[2m… fetching spaces\x1b[0m');
21-
let spaces;
22-
try {
23-
spaces = await fetchAccessibleSpaces(apiBaseUrl, apiKey);
24-
process.stdout.write('\r\x1b[K');
25-
} catch (e) {
26-
process.stdout.write('\r\x1b[K');
27-
die(`Could not list spaces: ${e.message}`);
28-
}
29-
if (!spaces.length) die('No spaces found.');
30-
31-
success(`${spaces.length} space(s) found`);
3219
saveConfig({ apiBaseUrl, apiKey });
3320

34-
await pickSpace('', spaces);
21+
const afterSpace = await pickSpace();
3522
const final = await pickThread();
3623
saveConfig({ ...final, apiBaseUrl, apiKey });
3724

lib/api.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,13 @@ async function request(baseUrl, apiKey, method, pathname, { params, body } = {})
3131
return data;
3232
}
3333

34-
export function listSpaces(baseUrl, apiKey, { scope = 'accessible', limit = 20, offset = 0 } = {}) {
34+
export function listSpaces(
35+
baseUrl,
36+
apiKey,
37+
{ scope = 'accessible', limit = 20, offset = 0, q, space_id } = {},
38+
) {
3539
return request(baseUrl, apiKey, 'GET', '/api/v1/me/spaces/', {
36-
params: { scope, limit, offset },
40+
params: { scope, limit, offset, q, space_id },
3741
});
3842
}
3943

lib/list-cli.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { fetchAccessibleSpaces, fetchThreads } from './fetch.js';
21
import { loadConfig } from './config.js';
2+
import { searchSpaces } from './spaces.js';
3+
import { fetchThreads } from './fetch.js';
34

45
export function filterItems(items, query, keys) {
56
const terms = (query || '').trim().toLowerCase().split(/\s+/).filter(Boolean);
@@ -15,18 +16,17 @@ export async function spacesForPrompt(filter = '', { limit = 4, offset = 0 } = {
1516
if (!cfg.apiKey || !cfg.apiBaseUrl) {
1617
throw new Error('Run mantis-setup first (API key + URL).');
1718
}
18-
const all = filterItems(
19-
await fetchAccessibleSpaces(cfg.apiBaseUrl, cfg.apiKey),
20-
filter,
21-
['name', 'id'],
22-
);
23-
const page = all.slice(offset, offset + limit);
19+
const page = await searchSpaces(cfg.apiBaseUrl, cfg.apiKey, {
20+
q: filter,
21+
limit,
22+
offset,
23+
});
2424
return {
25-
spaces: page,
26-
total: all.length,
25+
spaces: page.spaces || [],
26+
total: page.total,
2727
offset,
2828
limit,
29-
hasMore: offset + limit < all.length,
29+
hasMore: offset + limit < page.total,
3030
filter,
3131
};
3232
}

lib/picker.js

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
1-
import { createSpaceState, fetchAccessibleSpaces, fetchThreads } from './fetch.js';
1+
import { createSpaceState, fetchThreads } from './fetch.js';
22
import { loadConfig, saveConfig } from './config.js';
3-
import { filterItems } from './list-cli.js';
3+
import { searchSpaces, resolveSpaceFromInput } from './spaces.js';
4+
import { parseSpaceIdFromInput } from './space-id.js';
45
import { die, info, promptInput, promptSearch, success } from './ui.js';
56

7+
const BROWSE_PAGE = 20;
8+
69
function spaceLabel(s) {
710
const name = s.name || '(unnamed)';
811
const maps = s.map_count != null ? ` · ${s.map_count} map(s)` : '';
912
const role = s.role ? ` · ${s.role}` : '';
1013
return name + maps + role;
1114
}
1215

16+
function spaceChoices(spaces) {
17+
return spaces.map((s) => ({
18+
name: spaceLabel(s),
19+
value: s,
20+
description: s.id,
21+
}));
22+
}
23+
1324
export async function requireConfig() {
1425
const cfg = loadConfig();
1526
if (!cfg.apiKey || !cfg.apiBaseUrl) {
@@ -18,17 +29,14 @@ export async function requireConfig() {
1829
return cfg;
1930
}
2031

21-
function spaceChoices(spaces, query) {
22-
const list = filterItems(spaces, query, ['name', 'id']);
23-
return list.map((s) => ({
24-
name: spaceLabel(s),
25-
value: s,
26-
description: s.id,
27-
}));
28-
}
29-
3032
function threadChoices(threads, query) {
31-
const list = filterItems(threads, query, ['name', 'id']);
33+
const terms = (query || '').trim().toLowerCase();
34+
const list = terms
35+
? threads.filter((t) => {
36+
const hay = `${t.name} ${t.id}`.toLowerCase();
37+
return hay.includes(terms);
38+
})
39+
: threads;
3240
const out = [
3341
{ name: '➕ Create new thread', value: '__new__', description: 'New space state' },
3442
];
@@ -43,19 +51,45 @@ function threadChoices(threads, query) {
4351
return out;
4452
}
4553

46-
export async function pickSpace(initialFilter = '', preloadedSpaces = null) {
54+
async function pickSpaceByLink(cfg) {
55+
const raw = await promptInput('Paste Mantis space link or UUID (Enter to browse)', {
56+
default: '',
57+
});
58+
if (!raw?.trim()) return null;
59+
const space = await resolveSpaceFromInput(cfg.apiBaseUrl, cfg.apiKey, raw);
60+
if (!space) die('Space not found or you do not have access.');
61+
return saveSpace(cfg, space);
62+
}
63+
64+
export async function pickSpace() {
4765
const cfg = await requireConfig();
48-
const spaces = preloadedSpaces ?? (await fetchAccessibleSpaces(cfg.apiBaseUrl, cfg.apiKey));
49-
if (!spaces.length) die('No spaces found.');
5066

5167
if (!process.stdin.isTTY) {
52-
return saveSpace(cfg, spaces[0]);
68+
const page = await searchSpaces(cfg.apiBaseUrl, cfg.apiKey, { limit: 1 });
69+
if (!page.spaces?.length) die('No spaces found.');
70+
return saveSpace(cfg, page.spaces[0]);
5371
}
5472

73+
const fromLink = await pickSpaceByLink(cfg);
74+
if (fromLink) return fromLink;
75+
5576
const picked = await promptSearch(
56-
'Select space (↑↓ move, type to filter)',
57-
async (input) => spaceChoices(spaces, input || initialFilter),
77+
'Select space (↑↓ type to search, paste link in previous step)',
78+
async (input) => {
79+
const id = parseSpaceIdFromInput(input);
80+
if (id) {
81+
const one = await resolveSpaceFromInput(cfg.apiBaseUrl, cfg.apiKey, input);
82+
return one ? spaceChoices([one]) : [{ name: 'No match for that link/id', value: null, disabled: true }];
83+
}
84+
const page = await searchSpaces(cfg.apiBaseUrl, cfg.apiKey, {
85+
q: input || '',
86+
limit: BROWSE_PAGE,
87+
offset: 0,
88+
});
89+
return spaceChoices(page.spaces || []);
90+
},
5891
);
92+
if (!picked) die('No space selected.');
5993
return saveSpace(cfg, picked);
6094
}
6195

lib/space-id.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const UUID_RE =
2+
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
3+
4+
/** Extract space UUID from a Mantis URL (/space/{id}), raw UUID, or search text containing one. */
5+
export function parseSpaceIdFromInput(text) {
6+
const t = (text || '').trim();
7+
if (!t) return null;
8+
const pathMatch = t.match(/\/space\/([0-9a-f-]{36})/i);
9+
if (pathMatch) return pathMatch[1].toLowerCase();
10+
const uuidMatch = t.match(UUID_RE);
11+
if (uuidMatch) return uuidMatch[0].toLowerCase();
12+
return null;
13+
}

lib/spaces.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { listSpaces } from './api.js';
2+
import { parseSpaceIdFromInput } from './space-id.js';
3+
4+
export async function fetchSpaceById(baseUrl, apiKey, spaceId) {
5+
const page = await listSpaces(baseUrl, apiKey, {
6+
scope: 'accessible',
7+
space_id: spaceId,
8+
limit: 1,
9+
offset: 0,
10+
});
11+
return page.spaces?.[0] ?? null;
12+
}
13+
14+
export async function resolveSpaceFromInput(baseUrl, apiKey, text) {
15+
const id = parseSpaceIdFromInput(text);
16+
if (!id) return null;
17+
return fetchSpaceById(baseUrl, apiKey, id);
18+
}
19+
20+
/** One page of spaces; optional name/id search — does not load the full catalog. */
21+
export async function searchSpaces(
22+
baseUrl,
23+
apiKey,
24+
{ q = '', limit = 20, offset = 0, scope = 'accessible' } = {},
25+
) {
26+
const id = parseSpaceIdFromInput(q);
27+
if (id) {
28+
const one = await fetchSpaceById(baseUrl, apiKey, id);
29+
return {
30+
spaces: one ? [one] : [],
31+
total: one ? 1 : 0,
32+
limit: 1,
33+
offset: 0,
34+
};
35+
}
36+
const page = await listSpaces(baseUrl, apiKey, {
37+
scope,
38+
limit,
39+
offset,
40+
q: q.trim() || undefined,
41+
});
42+
return {
43+
spaces: page.spaces || [],
44+
total: page.total ?? (page.spaces || []).length,
45+
limit: page.limit ?? limit,
46+
offset: page.offset ?? offset,
47+
};
48+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mantis-claude-code",
3-
"version": "1.0.7",
3+
"version": "1.0.8",
44
"description": "Claude Code plugin for Mantis — MCP setup, space/thread selection, and platform skills",
55
"type": "module",
66
"license": "MIT",

0 commit comments

Comments
 (0)