mantisai-cli 2.0.1 → 3.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.
- package/README.md +26 -99
- package/bin/mantis.js +406 -23
- package/lib/constants.js +13 -0
- package/lib/container.js +63 -0
- package/lib/impl/claude-skills-service.js +13 -0
- package/lib/impl/fast-glob-codebase-indexer.js +114 -0
- package/lib/impl/file-config-store.js +38 -0
- package/lib/impl/fs-csv-reader.js +12 -0
- package/lib/impl/http-mantis-client.js +108 -0
- package/lib/impl/inquirer-ui-service.js +73 -0
- package/lib/impl/mcp-client-service.js +64 -0
- package/lib/impl/opencode-skills-service.js +17 -0
- package/lib/impl/space-repository.js +72 -0
- package/lib/interfaces/codebase-indexer.js +5 -0
- package/lib/interfaces/config-store.js +11 -0
- package/lib/interfaces/csv-reader.js +5 -0
- package/lib/interfaces/index.js +7 -0
- package/lib/interfaces/mantis-client.js +13 -0
- package/lib/interfaces/mcp-client.js +6 -0
- package/lib/interfaces/space-repository.js +13 -0
- package/lib/interfaces/ui.js +13 -0
- package/lib/services/context-service.js +22 -0
- package/lib/services/map-service.js +114 -0
- package/lib/services/query-service.js +50 -0
- package/lib/services/selection-service.js +174 -0
- package/lib/services/setup-service.js +81 -0
- package/lib/services/tool-service.js +26 -0
- package/lib/types.js +30 -0
- package/lib/utils/cli-args.js +29 -0
- package/lib/utils/package-root.js +7 -0
- package/lib/utils/skills-sync.js +43 -0
- package/lib/{space-id.js → utils/space-id.js} +0 -1
- package/lib/utils/threads.js +12 -0
- package/lib/utils/tool-args.js +38 -0
- package/lib/utils/url.js +33 -0
- package/package.json +4 -7
- package/skills/codebase/SKILL.md +8 -6
- package/skills/connect/SKILL.md +10 -26
- package/skills/createmap/SKILL.md +5 -3
- package/skills/mantis/SKILL.md +48 -32
- package/skills/select/SKILL.md +12 -9
- package/skills/space/SKILL.md +10 -47
- package/skills/status/SKILL.md +4 -9
- package/skills/thread/SKILL.md +15 -27
- package/.claude-plugin/marketplace.json +0 -14
- package/.claude-plugin/plugin.json +0 -18
- package/.mcp.json +0 -11
- package/bin/mantis-list-spaces.js +0 -32
- package/bin/mantis-list-threads.js +0 -32
- package/bin/mantis-mcp-headers.js +0 -9
- package/bin/mantis-pick-space.js +0 -5
- package/bin/mantis-pick-thread.js +0 -5
- package/bin/mantis-resolve-space.js +0 -25
- package/bin/mantis-select.js +0 -7
- package/bin/mantis-set-space.js +0 -31
- package/bin/mantis-set-thread.js +0 -34
- package/bin/mantis-setup.js +0 -59
- package/bin/mantis-status.js +0 -15
- package/lib/api.js +0 -100
- package/lib/claude-plugin.js +0 -150
- package/lib/codebase-csv.js +0 -115
- package/lib/config.js +0 -65
- package/lib/csv.js +0 -10
- package/lib/fetch.js +0 -36
- package/lib/list-cli.js +0 -55
- package/lib/map-create.js +0 -148
- package/lib/mcp-config.js +0 -50
- package/lib/picker.js +0 -150
- package/lib/spaces.js +0 -48
- package/lib/ui.js +0 -73
- /package/lib/{fields.js → utils/fields.js} +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { mcpUrl } from '../utils/url.js';
|
|
2
|
+
|
|
3
|
+
export class ContextService {
|
|
4
|
+
constructor({ configStore }) {
|
|
5
|
+
this.configStore = configStore;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
status() {
|
|
9
|
+
const cfg = this.configStore.load();
|
|
10
|
+
return {
|
|
11
|
+
configPath: this.configStore.configPath(),
|
|
12
|
+
apiBaseUrl: cfg.apiBaseUrl || '(not set — use mantis setup)',
|
|
13
|
+
mcpUrl: cfg.apiBaseUrl ? mcpUrl(cfg) : '(run setup)',
|
|
14
|
+
apiKeyHint: cfg.apiKey ? `***${cfg.apiKey.slice(-6)}` : '(not set)',
|
|
15
|
+
spaceName: cfg.spaceName || '-',
|
|
16
|
+
spaceId: cfg.spaceId || '-',
|
|
17
|
+
threadName: cfg.spaceStateName || '-',
|
|
18
|
+
threadId: cfg.spaceStateId || '-',
|
|
19
|
+
hasThread: Boolean(cfg.spaceStateId),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { fieldSummary, inferFieldTypes } from '../utils/fields.js';
|
|
4
|
+
import { boolOption } from '../utils/cli-args.js';
|
|
5
|
+
import { spaceUrl } from '../utils/url.js';
|
|
6
|
+
|
|
7
|
+
export class MapService {
|
|
8
|
+
constructor({ configStore, client, spaces, csvReader, ui }) {
|
|
9
|
+
this.configStore = configStore;
|
|
10
|
+
this.client = client;
|
|
11
|
+
this.spaces = spaces;
|
|
12
|
+
this.csvReader = csvReader;
|
|
13
|
+
this.ui = ui;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async _chooseExistingSpace(filter = '') {
|
|
17
|
+
const page = await this.spaces.search({ q: filter, limit: 12 });
|
|
18
|
+
if (!page.spaces?.length) throw new Error('No spaces found.');
|
|
19
|
+
return this.ui.promptSelect('Which space should receive this map?', page.spaces.map((s) => ({
|
|
20
|
+
name: `${s.name} · ${s.map_count ?? 0} map(s) · ${s.role || 'space'}`,
|
|
21
|
+
value: s,
|
|
22
|
+
description: s.id,
|
|
23
|
+
})));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async _resolveSpaceTarget(cfg, opts) {
|
|
27
|
+
let mode = opts.spaceMode;
|
|
28
|
+
if (!mode) {
|
|
29
|
+
mode = await this.ui.promptSelect('Where should this map go?', [
|
|
30
|
+
{ name: 'Create a new space', value: 'new' },
|
|
31
|
+
{ name: 'Add to an existing space', value: 'existing' },
|
|
32
|
+
]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (mode === 'existing') {
|
|
36
|
+
if (opts.spaceId) {
|
|
37
|
+
const space = opts.spaceName ? null : await this.spaces.fetchById(opts.spaceId);
|
|
38
|
+
return { spaceId: opts.spaceId, spaceName: opts.spaceName || space?.name };
|
|
39
|
+
}
|
|
40
|
+
const picked = await this._chooseExistingSpace(opts.spaceSearch || '');
|
|
41
|
+
return { spaceId: picked.id, spaceName: picked.name };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const spaceName = opts.spaceName || await this.ui.promptInput('New space name', {
|
|
45
|
+
default: path.basename(opts.file, path.extname(opts.file)) || 'Mantis Map',
|
|
46
|
+
});
|
|
47
|
+
const isPublic = opts.public != null
|
|
48
|
+
? boolOption(opts.public)
|
|
49
|
+
: await this.ui.promptConfirm('Make this space public?', { default: false });
|
|
50
|
+
return { spaceName, isPublic };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async createMap(file, opts = {}) {
|
|
54
|
+
const cfg = this.configStore.requireAuth();
|
|
55
|
+
const mapName = opts.mapName || await this.ui.promptInput('Map name', {
|
|
56
|
+
default: path.basename(file, path.extname(file)),
|
|
57
|
+
});
|
|
58
|
+
const headers = this.csvReader.readHeaders(file);
|
|
59
|
+
const dataTypes = opts.dataTypes ? JSON.parse(opts.dataTypes) : inferFieldTypes(headers, opts);
|
|
60
|
+
this.ui.info(`Fields: ${fieldSummary(headers, dataTypes).join(' | ')}`);
|
|
61
|
+
|
|
62
|
+
const target = await this._resolveSpaceTarget(cfg, { ...opts, file });
|
|
63
|
+
|
|
64
|
+
let spaceId = target.spaceId;
|
|
65
|
+
let spaceName = target.spaceName;
|
|
66
|
+
if (!spaceId) {
|
|
67
|
+
const space = await this.client.createSpace({ name: target.spaceName, isPublic: target.isPublic });
|
|
68
|
+
spaceId = space.id;
|
|
69
|
+
spaceName = space.name;
|
|
70
|
+
this.ui.info(`Created space: ${spaceName} (${spaceId})`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const result = await this.client.createMapInSpace(spaceId, {
|
|
74
|
+
file,
|
|
75
|
+
mapName,
|
|
76
|
+
dataTypes,
|
|
77
|
+
selectedFields: opts.selectedFields,
|
|
78
|
+
fieldWeights: opts.fieldWeights,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const primaryMapId = result.map_id || result.base_map_id || result.map_ids?.[0];
|
|
82
|
+
target.spaceName = spaceName;
|
|
83
|
+
this.ui.success('Map creation started');
|
|
84
|
+
this.ui.info(`Space: ${target.spaceName || spaceId} (${spaceId})`);
|
|
85
|
+
this.ui.info(`Map: ${mapName}${primaryMapId ? ` (${primaryMapId})` : ''}`);
|
|
86
|
+
if (spaceId) this.ui.info(`Link: ${spaceUrl(cfg.apiBaseUrl, spaceId)}`);
|
|
87
|
+
|
|
88
|
+
const activate = opts.activate != null
|
|
89
|
+
? boolOption(opts.activate)
|
|
90
|
+
: await this.ui.promptConfirm('Set this as the active Mantis space and thread?', { default: true });
|
|
91
|
+
let thread = null;
|
|
92
|
+
if (activate) {
|
|
93
|
+
const threadName = opts.threadName || await this.ui.promptInput('Thread name', { default: `${mapName} Exploration` });
|
|
94
|
+
thread = await this.client.createSpaceState(spaceId, threadName);
|
|
95
|
+
const next = {
|
|
96
|
+
...cfg,
|
|
97
|
+
spaceId,
|
|
98
|
+
spaceName: target.spaceName || mapName,
|
|
99
|
+
spaceStateId: thread.id,
|
|
100
|
+
spaceStateName: thread.name,
|
|
101
|
+
};
|
|
102
|
+
this.configStore.save(next);
|
|
103
|
+
this.ui.success(`Active thread: ${thread.name}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
...result,
|
|
108
|
+
space_id: spaceId,
|
|
109
|
+
map_id: primaryMapId,
|
|
110
|
+
space_url: spaceId ? spaceUrl(cfg.apiBaseUrl, spaceId) : undefined,
|
|
111
|
+
thread,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export function filterItems(items, query, keys) {
|
|
2
|
+
const terms = (query || '').trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
3
|
+
if (!terms.length) return items;
|
|
4
|
+
return items.filter((item) => {
|
|
5
|
+
const hay = keys.map((k) => String(item[k] ?? '')).join(' ').toLowerCase();
|
|
6
|
+
return terms.every((t) => hay.includes(t));
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class QueryService {
|
|
11
|
+
constructor({ configStore, spaces }) {
|
|
12
|
+
this.configStore = configStore;
|
|
13
|
+
this.spaces = spaces;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async spacesForPrompt(filter = '', { limit = 4, offset = 0 } = {}) {
|
|
17
|
+
this.configStore.requireAuth();
|
|
18
|
+
const page = await this.spaces.search({ q: filter, limit, offset });
|
|
19
|
+
return {
|
|
20
|
+
spaces: page.spaces || [],
|
|
21
|
+
total: page.total,
|
|
22
|
+
offset,
|
|
23
|
+
limit,
|
|
24
|
+
hasMore: offset + limit < page.total,
|
|
25
|
+
filter,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async threadsForPrompt(filter = '', { limit = 4, offset = 0 } = {}) {
|
|
30
|
+
const cfg = this.configStore.requireAuth();
|
|
31
|
+
if (!cfg.spaceId) throw new Error('Pick a space first (mantis select space).');
|
|
32
|
+
|
|
33
|
+
const all = filterItems(
|
|
34
|
+
await this.spaces.fetchThreads(cfg.spaceId),
|
|
35
|
+
filter,
|
|
36
|
+
['name', 'id'],
|
|
37
|
+
);
|
|
38
|
+
const page = all.slice(offset, offset + limit);
|
|
39
|
+
return {
|
|
40
|
+
threads: page,
|
|
41
|
+
total: all.length,
|
|
42
|
+
offset,
|
|
43
|
+
limit,
|
|
44
|
+
hasMore: offset + limit < all.length,
|
|
45
|
+
spaceId: cfg.spaceId,
|
|
46
|
+
spaceName: cfg.spaceName,
|
|
47
|
+
filter,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { BROWSE_PAGE, DEFAULT_THREAD_NAME } from '../constants.js';
|
|
2
|
+
import { parseSpaceIdFromInput } from '../utils/space-id.js';
|
|
3
|
+
import { defaultThreadName, findThreadByName } from '../utils/threads.js';
|
|
4
|
+
|
|
5
|
+
function spaceLabel(s) {
|
|
6
|
+
const name = s.name || '(unnamed)';
|
|
7
|
+
const maps = s.map_count != null ? ` · ${s.map_count} map(s)` : '';
|
|
8
|
+
const role = s.role ? ` · ${s.role}` : '';
|
|
9
|
+
return name + maps + role;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function spaceChoices(spaces) {
|
|
13
|
+
return spaces.map((s) => ({
|
|
14
|
+
name: spaceLabel(s),
|
|
15
|
+
value: s,
|
|
16
|
+
description: s.id,
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class SelectionService {
|
|
21
|
+
constructor({ configStore, spaces, client, ui }) {
|
|
22
|
+
this.configStore = configStore;
|
|
23
|
+
this.spaces = spaces;
|
|
24
|
+
this.client = client;
|
|
25
|
+
this.ui = ui;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_saveSpace(cfg, space) {
|
|
29
|
+
const changed = cfg.spaceId !== space.id;
|
|
30
|
+
const next = { ...cfg, spaceId: space.id, spaceName: space.name };
|
|
31
|
+
if (changed) {
|
|
32
|
+
delete next.spaceStateId;
|
|
33
|
+
delete next.spaceStateName;
|
|
34
|
+
}
|
|
35
|
+
this.configStore.save(next);
|
|
36
|
+
this.ui.success(`Space: ${space.name}`);
|
|
37
|
+
if (changed) this.ui.info('Thread cleared — run: mantis select thread');
|
|
38
|
+
return next;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_saveThread(cfg, thread) {
|
|
42
|
+
const next = { ...cfg, spaceStateId: thread.id, spaceStateName: thread.name };
|
|
43
|
+
this.configStore.save(next);
|
|
44
|
+
this.ui.success(`Thread: ${thread.name}`);
|
|
45
|
+
return next;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_threadChoices(threads, query) {
|
|
49
|
+
const terms = (query || '').trim().toLowerCase();
|
|
50
|
+
const list = terms
|
|
51
|
+
? threads.filter((t) => `${t.name} ${t.id}`.toLowerCase().includes(terms))
|
|
52
|
+
: threads;
|
|
53
|
+
const out = [{ name: '➕ Create new thread', value: '__new__', description: 'New space state' }];
|
|
54
|
+
for (const t of list) {
|
|
55
|
+
const when = t.updated_at ? String(t.updated_at).slice(0, 10) : '';
|
|
56
|
+
out.push({ name: t.name || '(unnamed)', value: t, description: when || t.id });
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async _pickSpaceByLink(cfg) {
|
|
62
|
+
const raw = await this.ui.promptInput('Paste Mantis space link or UUID (Enter to browse)', { default: '' });
|
|
63
|
+
if (!raw?.trim()) return null;
|
|
64
|
+
const space = await this.spaces.resolveFromInput(raw);
|
|
65
|
+
if (!space) this.ui.die('Space not found or you do not have access.');
|
|
66
|
+
return this._saveSpace(cfg, space);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async pickSpace(initialFilter = '') {
|
|
70
|
+
const cfg = this.configStore.requireAuth();
|
|
71
|
+
|
|
72
|
+
if (!process.stdin.isTTY) {
|
|
73
|
+
const page = await this.spaces.search({ q: initialFilter, limit: 1 });
|
|
74
|
+
if (!page.spaces?.length) this.ui.die('No spaces found.');
|
|
75
|
+
return this._saveSpace(cfg, page.spaces[0]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const fromLink = await this._pickSpaceByLink(cfg);
|
|
79
|
+
if (fromLink) return fromLink;
|
|
80
|
+
|
|
81
|
+
const picked = await this.ui.promptSearch(
|
|
82
|
+
'Select space (↑↓ type to search, paste link in previous step)',
|
|
83
|
+
async (input) => {
|
|
84
|
+
const id = parseSpaceIdFromInput(input);
|
|
85
|
+
if (id) {
|
|
86
|
+
const one = await this.spaces.resolveFromInput(input);
|
|
87
|
+
return one ? spaceChoices([one]) : [{ name: 'No match for that link/id', value: null, disabled: true }];
|
|
88
|
+
}
|
|
89
|
+
const page = await this.spaces.search({
|
|
90
|
+
q: input || initialFilter || '',
|
|
91
|
+
limit: BROWSE_PAGE,
|
|
92
|
+
offset: 0,
|
|
93
|
+
});
|
|
94
|
+
return spaceChoices(page.spaces || []);
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
if (!picked) this.ui.die('No space selected.');
|
|
98
|
+
return this._saveSpace(cfg, picked);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async pickThread(initialFilter = '') {
|
|
102
|
+
const cfg = this.configStore.requireAuth();
|
|
103
|
+
if (!cfg.spaceId) this.ui.die('Pick a space first: mantis select space');
|
|
104
|
+
|
|
105
|
+
const threads = await this.spaces.fetchThreads(cfg.spaceId);
|
|
106
|
+
|
|
107
|
+
if (!process.stdin.isTTY) {
|
|
108
|
+
if (!threads.length) this.ui.die('No threads. Run interactively to create one.');
|
|
109
|
+
return this._saveThread(cfg, threads[0]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const picked = await this.ui.promptSearch(
|
|
113
|
+
`Thread in "${cfg.spaceName || 'space'}" (↑↓, type to filter)`,
|
|
114
|
+
async (input) => this._threadChoices(threads, input || initialFilter),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (picked === '__new__') {
|
|
118
|
+
const defaultName = defaultThreadName(DEFAULT_THREAD_NAME, threads);
|
|
119
|
+
const name = await this.ui.promptInput('Thread name', { default: defaultName });
|
|
120
|
+
return this._createThread(cfg, threads, name || defaultName);
|
|
121
|
+
}
|
|
122
|
+
return this._saveThread(cfg, picked);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async _createThread(cfg, threads, name) {
|
|
126
|
+
const trimmed = String(name || DEFAULT_THREAD_NAME).trim() || DEFAULT_THREAD_NAME;
|
|
127
|
+
const existing = findThreadByName(threads, trimmed);
|
|
128
|
+
if (existing) {
|
|
129
|
+
this.ui.info(`Thread "${trimmed}" already exists — using it.`);
|
|
130
|
+
return this._saveThread(cfg, existing);
|
|
131
|
+
}
|
|
132
|
+
const created = await this.client.createSpaceState(cfg.spaceId, trimmed);
|
|
133
|
+
return this._saveThread(cfg, created);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
setSpace(id, name = '') {
|
|
137
|
+
const cfg = this.configStore.load();
|
|
138
|
+
const changed = cfg.spaceId !== id;
|
|
139
|
+
const next = {
|
|
140
|
+
...cfg,
|
|
141
|
+
spaceId: id,
|
|
142
|
+
spaceName: name || cfg.spaceName,
|
|
143
|
+
...(changed ? { spaceStateId: undefined, spaceStateName: undefined } : {}),
|
|
144
|
+
};
|
|
145
|
+
this.configStore.save(next);
|
|
146
|
+
return { ok: true, spaceId: id, spaceName: name, threadCleared: changed, needThread: changed };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async setThread(arg, arg2) {
|
|
150
|
+
const cfg = this.configStore.requireAuth();
|
|
151
|
+
if (!cfg.spaceId) throw new Error('Run mantis setup and pick a space first.');
|
|
152
|
+
|
|
153
|
+
let thread;
|
|
154
|
+
if (arg === '--new') {
|
|
155
|
+
const threads = await this.spaces.fetchThreads(cfg.spaceId);
|
|
156
|
+
const name = arg2 || defaultThreadName(DEFAULT_THREAD_NAME, threads);
|
|
157
|
+
const existing = findThreadByName(threads, name);
|
|
158
|
+
thread = existing || await this.client.createSpaceState(cfg.spaceId, name);
|
|
159
|
+
} else {
|
|
160
|
+
thread = { id: arg, name: arg2 || arg };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const next = { ...cfg, spaceStateId: thread.id, spaceStateName: thread.name };
|
|
164
|
+
this.configStore.save(next);
|
|
165
|
+
return { ok: true, spaceStateId: thread.id, name: thread.name };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async resolveSpace(input) {
|
|
169
|
+
this.configStore.requireAuth();
|
|
170
|
+
const space = await this.spaces.resolveFromInput(input);
|
|
171
|
+
if (!space) throw new Error('Space not found or no access');
|
|
172
|
+
return { space, threadCleared: true };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { DEFAULT_API_BASE, DEVELOPER_PORTAL_URL } from '../constants.js';
|
|
2
|
+
import { normalizeBaseUrl, mcpUrl } from '../utils/url.js';
|
|
3
|
+
|
|
4
|
+
const PROVIDERS = new Set(['claude', 'opencode']);
|
|
5
|
+
|
|
6
|
+
export class SetupService {
|
|
7
|
+
constructor({ configStore, selection, claudeSkills, opencodeSkills, ui }) {
|
|
8
|
+
this.configStore = configStore;
|
|
9
|
+
this.selection = selection;
|
|
10
|
+
this.claudeSkills = claudeSkills;
|
|
11
|
+
this.opencodeSkills = opencodeSkills;
|
|
12
|
+
this.ui = ui;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async run() {
|
|
16
|
+
const prev = this.configStore.load();
|
|
17
|
+
this.ui.banner('Mantis setup', 'API credentials and workspace context');
|
|
18
|
+
|
|
19
|
+
const apiBaseUrl = normalizeBaseUrl(
|
|
20
|
+
await this.ui.promptInput('Mantis API URL', {
|
|
21
|
+
default: prev.apiBaseUrl || process.env.MANTIS_API_URL || DEFAULT_API_BASE,
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
this.ui.info(`API keys: ${DEVELOPER_PORTAL_URL}`);
|
|
25
|
+
const apiKey = (await this.ui.promptSecret('API key (Ctrl+click link above to open portal)', {
|
|
26
|
+
default: prev.apiKey,
|
|
27
|
+
}))?.trim();
|
|
28
|
+
if (!apiKey) this.ui.die('API key is required.');
|
|
29
|
+
|
|
30
|
+
let cfg = { ...prev, apiBaseUrl, apiKey };
|
|
31
|
+
this.configStore.save(cfg);
|
|
32
|
+
|
|
33
|
+
if (!cfg.spaceId) {
|
|
34
|
+
cfg = await this.selection.pickSpace();
|
|
35
|
+
} else {
|
|
36
|
+
this.ui.info(`Space: ${cfg.spaceName || cfg.spaceId} (unchanged)`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!cfg.spaceStateId) {
|
|
40
|
+
if (!cfg.spaceId) this.ui.die('Pick a space before choosing a thread.');
|
|
41
|
+
cfg = await this.selection.pickThread();
|
|
42
|
+
} else {
|
|
43
|
+
this.ui.info(`Thread: ${cfg.spaceStateName || cfg.spaceStateId} (unchanged)`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.configStore.save(cfg);
|
|
47
|
+
|
|
48
|
+
console.log('');
|
|
49
|
+
this.ui.success('Setup complete');
|
|
50
|
+
this.ui.info(`Space: ${cfg.spaceName || '-'} (${cfg.spaceId || '-'})`);
|
|
51
|
+
this.ui.info(`Thread: ${cfg.spaceStateName || '-'} (${cfg.spaceStateId || '-'})`);
|
|
52
|
+
this.ui.info(`MCP: ${mcpUrl(cfg)}`);
|
|
53
|
+
this.ui.info('Explore with: mantis tools && mantis use get_space_context');
|
|
54
|
+
this.ui.info('Editor skills: mantis setup claude | mantis setup opencode');
|
|
55
|
+
console.log('');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
runProvider(provider) {
|
|
59
|
+
const p = String(provider || '').toLowerCase();
|
|
60
|
+
if (!PROVIDERS.has(p)) {
|
|
61
|
+
throw new Error(`Unknown provider "${provider}". Use: claude, opencode`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (p === 'claude') {
|
|
65
|
+
const { skillsDir, installed } = this.claudeSkills.sync();
|
|
66
|
+
this.ui.banner('Mantis skills → Claude Code');
|
|
67
|
+
this.ui.success(`Synced ${installed.length} skill(s) to ${skillsDir}`);
|
|
68
|
+
for (const s of installed) this.ui.info(`${s.slash} ← skills/${s.source}`);
|
|
69
|
+
this.ui.info('Try /mantis or /mantis-connect in Claude Code.');
|
|
70
|
+
return { provider: p, skillsDir, installed };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { globalSkillsDir, projectSkillsDir, installed } = this.opencodeSkills.sync();
|
|
74
|
+
this.ui.banner('Mantis skills → OpenCode');
|
|
75
|
+
this.ui.success(`Synced ${installed.length} skill(s)`);
|
|
76
|
+
this.ui.info(`Global: ${globalSkillsDir}`);
|
|
77
|
+
this.ui.info(`Project: ${projectSkillsDir}`);
|
|
78
|
+
for (const s of installed) this.ui.info(`${s.slash} ← skills/${s.source}`);
|
|
79
|
+
return { provider: p, globalSkillsDir, projectSkillsDir, installed };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { DISABLED_MCP_TOOLS } from '../constants.js';
|
|
2
|
+
|
|
3
|
+
function disabledToolError(name) {
|
|
4
|
+
if (name === 'create_space') {
|
|
5
|
+
return 'create_space is disabled in the CLI. Use: mantis setup or mantis select space';
|
|
6
|
+
}
|
|
7
|
+
return `${name} is disabled in the CLI. Use: mantis create map or mantis create codebase`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ToolService {
|
|
11
|
+
constructor({ mcp }) {
|
|
12
|
+
this.mcp = mcp;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async listTools() {
|
|
16
|
+
const data = await this.mcp.listTools();
|
|
17
|
+
return {
|
|
18
|
+
tools: (data.tools || []).filter((t) => !DISABLED_MCP_TOOLS.has(t.name)),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
useTool(name, args = {}) {
|
|
23
|
+
if (DISABLED_MCP_TOOLS.has(name)) throw new Error(disabledToolError(name));
|
|
24
|
+
return this.mcp.callTool(name, args);
|
|
25
|
+
}
|
|
26
|
+
}
|
package/lib/types.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** @typedef {Object} MantisConfig
|
|
2
|
+
* @property {string} [apiBaseUrl]
|
|
3
|
+
* @property {string} [apiKey]
|
|
4
|
+
* @property {string} [spaceId]
|
|
5
|
+
* @property {string} [spaceName]
|
|
6
|
+
* @property {string} [spaceStateId]
|
|
7
|
+
* @property {string} [spaceStateName]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** @typedef {Object} Space
|
|
11
|
+
* @property {string} id
|
|
12
|
+
* @property {string} name
|
|
13
|
+
* @property {number} [map_count]
|
|
14
|
+
* @property {string} [role]
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** @typedef {Object} Thread
|
|
18
|
+
* @property {string} id
|
|
19
|
+
* @property {string} name
|
|
20
|
+
* @property {string} [updated_at]
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** @typedef {Object} SpacePage
|
|
24
|
+
* @property {Space[]} spaces
|
|
25
|
+
* @property {number} total
|
|
26
|
+
* @property {number} [limit]
|
|
27
|
+
* @property {number} [offset]
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function parseListArgs(argv, { defaultLimit = 4 } = {}) {
|
|
2
|
+
let filter = '';
|
|
3
|
+
let offset = 0;
|
|
4
|
+
let limit = defaultLimit;
|
|
5
|
+
for (let i = 0; i < argv.length; i++) {
|
|
6
|
+
const a = argv[i];
|
|
7
|
+
if (a === '--offset') {
|
|
8
|
+
offset = parseInt(argv[++i], 10) || 0;
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
if (a === '--limit') {
|
|
12
|
+
limit = parseInt(argv[++i], 10) || defaultLimit;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (a === '--filter') {
|
|
16
|
+
filter = argv[++i] ?? '';
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
filter = argv.slice(i).join(' ').trim();
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
return { filter, offset, limit };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function boolOption(value, fallback = false) {
|
|
26
|
+
if (value == null) return fallback;
|
|
27
|
+
if (typeof value === 'boolean') return value;
|
|
28
|
+
return ['1', 'true', 'yes', 'public'].includes(String(value).toLowerCase());
|
|
29
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
|
|
6
|
+
export const PACKAGE_ROOT = path.resolve(__dirname, '../..');
|
|
7
|
+
export const SKILLS_DIR = path.join(PACKAGE_ROOT, 'skills');
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export function installSkillName(sourceDir) {
|
|
5
|
+
return sourceDir === 'mantis' ? 'mantis' : `mantis-${sourceDir}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function listSkillSources(skillsDir) {
|
|
9
|
+
return fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
10
|
+
.filter((d) => d.isDirectory() && fs.existsSync(path.join(skillsDir, d.name, 'SKILL.md')))
|
|
11
|
+
.map((d) => d.name)
|
|
12
|
+
.sort();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function prepareSkillContent(content, installName) {
|
|
16
|
+
if (!content.startsWith('---\n')) {
|
|
17
|
+
return `---\nname: ${installName}\n---\n\n${content}`;
|
|
18
|
+
}
|
|
19
|
+
const match = content.match(/^---\n([\s\S]*?)\n---([\s\S]*)$/);
|
|
20
|
+
if (!match) return content;
|
|
21
|
+
let yaml = match[1];
|
|
22
|
+
const body = match[2];
|
|
23
|
+
yaml = /^name:\s/m.test(yaml)
|
|
24
|
+
? yaml.replace(/^name:\s.*$/m, `name: ${installName}`)
|
|
25
|
+
: `name: ${installName}\n${yaml}`;
|
|
26
|
+
return `---\n${yaml}\n---${body}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function syncSkills({ skillsDir, targets, installName = installSkillName }) {
|
|
30
|
+
const installed = [];
|
|
31
|
+
for (const source of listSkillSources(skillsDir)) {
|
|
32
|
+
const name = installName(source);
|
|
33
|
+
const raw = fs.readFileSync(path.join(skillsDir, source, 'SKILL.md'), 'utf8');
|
|
34
|
+
const content = prepareSkillContent(raw, name);
|
|
35
|
+
for (const root of targets) {
|
|
36
|
+
const dest = path.join(root, name);
|
|
37
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
38
|
+
fs.writeFileSync(path.join(dest, 'SKILL.md'), content);
|
|
39
|
+
}
|
|
40
|
+
installed.push({ source, name, slash: `/${name}` });
|
|
41
|
+
}
|
|
42
|
+
return installed;
|
|
43
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const UUID_RE =
|
|
2
2
|
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
3
3
|
|
|
4
|
-
/** Extract space UUID from a Mantis URL (/space/{id}), raw UUID, or search text containing one. */
|
|
5
4
|
export function parseSpaceIdFromInput(text) {
|
|
6
5
|
const t = (text || '').trim();
|
|
7
6
|
if (!t) return null;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function findThreadByName(threads, name) {
|
|
2
|
+
return threads.find((t) => t.name === name) ?? null;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function defaultThreadName(base, threads) {
|
|
6
|
+
if (!findThreadByName(threads, base)) return base;
|
|
7
|
+
for (let n = 2; n < 100; n++) {
|
|
8
|
+
const candidate = `${base} ${n}`;
|
|
9
|
+
if (!findThreadByName(threads, candidate)) return candidate;
|
|
10
|
+
}
|
|
11
|
+
return `${base} ${Date.now()}`;
|
|
12
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function parseToolArgs(argv) {
|
|
2
|
+
const args = {};
|
|
3
|
+
for (let i = 0; i < argv.length; i++) {
|
|
4
|
+
const a = argv[i];
|
|
5
|
+
if (a === '--args') {
|
|
6
|
+
Object.assign(args, JSON.parse(argv[++i] || '{}'));
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
if (!a.startsWith('--')) continue;
|
|
10
|
+
const key = a.slice(2).replace(/-/g, '_');
|
|
11
|
+
const next = argv[i + 1];
|
|
12
|
+
if (next && !next.startsWith('--')) {
|
|
13
|
+
args[key] = coerceValue(next);
|
|
14
|
+
i++;
|
|
15
|
+
} else {
|
|
16
|
+
args[key] = true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return args;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function coerceValue(v) {
|
|
23
|
+
if (v === 'true') return true;
|
|
24
|
+
if (v === 'false') return false;
|
|
25
|
+
if (v !== '' && !Number.isNaN(Number(v))) return Number(v);
|
|
26
|
+
return v;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function parseUseCommand(argv) {
|
|
30
|
+
const idx = argv.indexOf('use');
|
|
31
|
+
if (idx === -1 || !argv[idx + 1]) {
|
|
32
|
+
throw new Error('Usage: mantis use <tool> [--args JSON] [--key value ...]');
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
tool: argv[idx + 1],
|
|
36
|
+
args: parseToolArgs(argv.slice(idx + 2)),
|
|
37
|
+
};
|
|
38
|
+
}
|
package/lib/utils/url.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DEFAULT_API_BASE } from '../constants.js';
|
|
2
|
+
|
|
3
|
+
export function normalizeBaseUrl(url) {
|
|
4
|
+
return String(url || '').trim().replace(/\/+$/, '');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function mcpUrl(cfg) {
|
|
8
|
+
const base = normalizeBaseUrl(cfg.apiBaseUrl || process.env.MANTIS_API_URL || DEFAULT_API_BASE);
|
|
9
|
+
return `${base}/mcp_integrated/`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function frontendBaseUrl(apiBaseUrl) {
|
|
13
|
+
const root = normalizeBaseUrl(apiBaseUrl).replace(/\/api\/?$/, '');
|
|
14
|
+
let url;
|
|
15
|
+
try {
|
|
16
|
+
url = new URL(root);
|
|
17
|
+
} catch {
|
|
18
|
+
return root;
|
|
19
|
+
}
|
|
20
|
+
if (/^(localhost|127\.0\.0\.1)$/i.test(url.hostname)) {
|
|
21
|
+
url.port = '3000';
|
|
22
|
+
return url.origin;
|
|
23
|
+
}
|
|
24
|
+
if (/(^|\.)kellis-h200-1\.csail\.mit\.edu$/i.test(url.hostname)) {
|
|
25
|
+
url.hostname = 'mantis.csail.mit.edu';
|
|
26
|
+
return url.origin;
|
|
27
|
+
}
|
|
28
|
+
return root;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function spaceUrl(apiBaseUrl, spaceId) {
|
|
32
|
+
return `${frontendBaseUrl(apiBaseUrl)}/space/${spaceId}`;
|
|
33
|
+
}
|