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.
Files changed (71) hide show
  1. package/README.md +26 -99
  2. package/bin/mantis.js +406 -23
  3. package/lib/constants.js +13 -0
  4. package/lib/container.js +63 -0
  5. package/lib/impl/claude-skills-service.js +13 -0
  6. package/lib/impl/fast-glob-codebase-indexer.js +114 -0
  7. package/lib/impl/file-config-store.js +38 -0
  8. package/lib/impl/fs-csv-reader.js +12 -0
  9. package/lib/impl/http-mantis-client.js +108 -0
  10. package/lib/impl/inquirer-ui-service.js +73 -0
  11. package/lib/impl/mcp-client-service.js +64 -0
  12. package/lib/impl/opencode-skills-service.js +17 -0
  13. package/lib/impl/space-repository.js +72 -0
  14. package/lib/interfaces/codebase-indexer.js +5 -0
  15. package/lib/interfaces/config-store.js +11 -0
  16. package/lib/interfaces/csv-reader.js +5 -0
  17. package/lib/interfaces/index.js +7 -0
  18. package/lib/interfaces/mantis-client.js +13 -0
  19. package/lib/interfaces/mcp-client.js +6 -0
  20. package/lib/interfaces/space-repository.js +13 -0
  21. package/lib/interfaces/ui.js +13 -0
  22. package/lib/services/context-service.js +22 -0
  23. package/lib/services/map-service.js +114 -0
  24. package/lib/services/query-service.js +50 -0
  25. package/lib/services/selection-service.js +174 -0
  26. package/lib/services/setup-service.js +81 -0
  27. package/lib/services/tool-service.js +26 -0
  28. package/lib/types.js +30 -0
  29. package/lib/utils/cli-args.js +29 -0
  30. package/lib/utils/package-root.js +7 -0
  31. package/lib/utils/skills-sync.js +43 -0
  32. package/lib/{space-id.js → utils/space-id.js} +0 -1
  33. package/lib/utils/threads.js +12 -0
  34. package/lib/utils/tool-args.js +38 -0
  35. package/lib/utils/url.js +33 -0
  36. package/package.json +4 -7
  37. package/skills/codebase/SKILL.md +8 -6
  38. package/skills/connect/SKILL.md +10 -26
  39. package/skills/createmap/SKILL.md +5 -3
  40. package/skills/mantis/SKILL.md +48 -32
  41. package/skills/select/SKILL.md +12 -9
  42. package/skills/space/SKILL.md +10 -47
  43. package/skills/status/SKILL.md +4 -9
  44. package/skills/thread/SKILL.md +15 -27
  45. package/.claude-plugin/marketplace.json +0 -14
  46. package/.claude-plugin/plugin.json +0 -18
  47. package/.mcp.json +0 -11
  48. package/bin/mantis-list-spaces.js +0 -32
  49. package/bin/mantis-list-threads.js +0 -32
  50. package/bin/mantis-mcp-headers.js +0 -9
  51. package/bin/mantis-pick-space.js +0 -5
  52. package/bin/mantis-pick-thread.js +0 -5
  53. package/bin/mantis-resolve-space.js +0 -25
  54. package/bin/mantis-select.js +0 -7
  55. package/bin/mantis-set-space.js +0 -31
  56. package/bin/mantis-set-thread.js +0 -34
  57. package/bin/mantis-setup.js +0 -59
  58. package/bin/mantis-status.js +0 -15
  59. package/lib/api.js +0 -100
  60. package/lib/claude-plugin.js +0 -150
  61. package/lib/codebase-csv.js +0 -115
  62. package/lib/config.js +0 -65
  63. package/lib/csv.js +0 -10
  64. package/lib/fetch.js +0 -36
  65. package/lib/list-cli.js +0 -55
  66. package/lib/map-create.js +0 -148
  67. package/lib/mcp-config.js +0 -50
  68. package/lib/picker.js +0 -150
  69. package/lib/spaces.js +0 -48
  70. package/lib/ui.js +0 -73
  71. /package/lib/{fields.js → utils/fields.js} +0 -0
@@ -0,0 +1,63 @@
1
+ import { FileConfigStore } from './impl/file-config-store.js';
2
+ import { HttpMantisClient } from './impl/http-mantis-client.js';
3
+ import { McpClientService } from './impl/mcp-client-service.js';
4
+ import { SpaceRepositoryImpl } from './impl/space-repository.js';
5
+ import { InquirerUiService } from './impl/inquirer-ui-service.js';
6
+ import { FastGlobCodebaseIndexer } from './impl/fast-glob-codebase-indexer.js';
7
+ import { FsCsvReader } from './impl/fs-csv-reader.js';
8
+ import { ClaudeSkillsService } from './impl/claude-skills-service.js';
9
+ import { OpencodeSkillsService } from './impl/opencode-skills-service.js';
10
+ import { SelectionService } from './services/selection-service.js';
11
+ import { SetupService } from './services/setup-service.js';
12
+ import { MapService } from './services/map-service.js';
13
+ import { QueryService } from './services/query-service.js';
14
+ import { ContextService } from './services/context-service.js';
15
+ import { ToolService } from './services/tool-service.js';
16
+
17
+ export function createContainer(overrides = {}) {
18
+ const configStore = overrides.configStore ?? new FileConfigStore();
19
+ const client = overrides.client ?? new HttpMantisClient(configStore);
20
+ const spaces = overrides.spaces ?? new SpaceRepositoryImpl(client);
21
+ const ui = overrides.ui ?? new InquirerUiService();
22
+ const mcp = overrides.mcp ?? new McpClientService(configStore);
23
+ const codebaseIndexer = overrides.codebaseIndexer ?? new FastGlobCodebaseIndexer();
24
+ const csvReader = overrides.csvReader ?? new FsCsvReader();
25
+ const claudeSkills = overrides.claudeSkills ?? new ClaudeSkillsService();
26
+ const opencodeSkills = overrides.opencodeSkills ?? new OpencodeSkillsService();
27
+
28
+ const selection = new SelectionService({ configStore, spaces, client, ui });
29
+ const setup = new SetupService({ configStore, selection, claudeSkills, opencodeSkills, ui });
30
+ const map = new MapService({ configStore, client, spaces, csvReader, ui });
31
+ const query = new QueryService({ configStore, spaces });
32
+ const context = new ContextService({ configStore });
33
+ const tools = new ToolService({ mcp });
34
+
35
+ return {
36
+ configStore,
37
+ client,
38
+ spaces,
39
+ ui,
40
+ mcp,
41
+ codebaseIndexer,
42
+ csvReader,
43
+ claudeSkills,
44
+ opencodeSkills,
45
+ selection,
46
+ setup,
47
+ map,
48
+ query,
49
+ context,
50
+ tools,
51
+ };
52
+ }
53
+
54
+ let _container;
55
+
56
+ export function getContainer() {
57
+ if (!_container) _container = createContainer();
58
+ return _container;
59
+ }
60
+
61
+ export function resetContainer() {
62
+ _container = undefined;
63
+ }
@@ -0,0 +1,13 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+
4
+ import { SKILLS_DIR } from '../utils/package-root.js';
5
+ import { syncSkills } from '../utils/skills-sync.js';
6
+
7
+ export class ClaudeSkillsService {
8
+ sync() {
9
+ const skillsDir = path.join(os.homedir(), '.claude', 'skills');
10
+ const installed = syncSkills({ skillsDir: SKILLS_DIR, targets: [skillsDir] });
11
+ return { skillsDir, installed };
12
+ }
13
+ }
@@ -0,0 +1,114 @@
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/**', '**/.next/**', '**/.turbo/**', '**/.venv/**', '**/__pycache__/**',
8
+ '**/build/**', '**/coverage/**', '**/dist/**', '**/node_modules/**', '**/vendor/**',
9
+ '**/*.lock', '**/package-lock.json', '**/pnpm-lock.yaml', '**/yarn.lock',
10
+ '**/.DS_Store', '**/Thumbs.db',
11
+ ];
12
+
13
+ const EXT_LANGUAGE = {
14
+ '.css': 'css', '.go': 'go', '.html': 'html', '.java': 'java', '.js': 'javascript',
15
+ '.jsx': 'javascript', '.json': 'json', '.md': 'markdown', '.py': 'python', '.rs': 'rust',
16
+ '.scss': 'scss', '.sql': 'sql', '.ts': 'typescript', '.tsx': 'typescript', '.vue': 'vue',
17
+ '.yaml': 'yaml', '.yml': 'yaml',
18
+ };
19
+
20
+ const SOURCE_EXTENSIONS = Object.keys(EXT_LANGUAGE);
21
+ const NULL_BYTE = String.fromCharCode(0);
22
+
23
+ function inferKind(rel) {
24
+ const base = path.basename(rel).toLowerCase();
25
+ if (base.includes('test') || base.includes('spec')) return 'test';
26
+ if (rel.includes('/components/') || rel.includes('\\components\\')) return 'component';
27
+ if (rel.includes('/pages/') || rel.includes('\\pages\\') || rel.includes('/app/')) return 'route';
28
+ if (rel.includes('/api/') || rel.includes('\\api\\')) return 'api';
29
+ if (rel.includes('/hooks/') || rel.includes('\\hooks\\')) return 'hook';
30
+ if (rel.includes('/utils/') || rel.includes('\\utils\\') || rel.includes('/lib/')) return 'utility';
31
+ return 'source';
32
+ }
33
+
34
+ function importsFrom(content) {
35
+ const imports = new Set();
36
+ const patterns = [
37
+ /^\s*import\s+.*?\s+from\s+['"]([^'"]+)['"]/gm,
38
+ /^\s*import\s+['"]([^'"]+)['"]/gm,
39
+ /^\s*const\s+.*?=\s+require\(['"]([^'"]+)['"]\)/gm,
40
+ /^\s*from\s+([\w.]+)\s+import\s+/gm,
41
+ ];
42
+ for (const re of patterns) {
43
+ for (const match of content.matchAll(re)) imports.add(match[1]);
44
+ }
45
+ return [...imports].slice(0, 40).join(', ');
46
+ }
47
+
48
+ function summarize(content) {
49
+ const lines = content.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
50
+ const comment = lines.find((l) => /^\/\/|^#|^\/\*|^\*|^<!--/.test(l));
51
+ return (comment || lines[0] || '').replace(/^\/\/|^#|^\/\*+|^\*|<!--|-->/g, '').trim().slice(0, 240);
52
+ }
53
+
54
+ function isBinaryBuffer(buf) {
55
+ const sample = buf.subarray(0, Math.min(buf.length, 8192));
56
+ for (let i = 0; i < sample.length; i++) {
57
+ const b = sample[i];
58
+ if (b === 0) return true;
59
+ if (b < 9 || (b > 13 && b < 32)) return true;
60
+ }
61
+ return false;
62
+ }
63
+
64
+ function readText(file, maxChars) {
65
+ const buf = fs.readFileSync(file);
66
+ if (isBinaryBuffer(buf)) return null;
67
+ let raw = buf.toString('utf8');
68
+ if (raw.indexOf(NULL_BYTE) !== -1) raw = raw.split(NULL_BYTE).join('');
69
+ return raw.length > maxChars ? `${raw.slice(0, maxChars)}\n\n[truncated]` : raw;
70
+ }
71
+
72
+ export class FastGlobCodebaseIndexer {
73
+ async index(rootDir, outFile, { maxChars = 12000, include } = {}) {
74
+ const root = path.resolve(rootDir || process.cwd());
75
+ const patterns = include?.length ? include : SOURCE_EXTENSIONS.map((ext) => `**/*${ext}`);
76
+ const files = await fg(patterns, {
77
+ cwd: root,
78
+ absolute: true,
79
+ dot: false,
80
+ ignore: DEFAULT_IGNORES,
81
+ onlyFiles: true,
82
+ });
83
+
84
+ const skipped = [];
85
+ const rows = [];
86
+ for (const file of files.sort()) {
87
+ const rel = path.relative(root, file).replace(/\\/g, '/');
88
+ const ext = path.extname(file).toLowerCase();
89
+ const content = readText(file, maxChars);
90
+ if (content === null) {
91
+ skipped.push(rel);
92
+ continue;
93
+ }
94
+ const stat = fs.statSync(file);
95
+ rows.push({
96
+ path: rel,
97
+ file_name: path.basename(file),
98
+ extension: ext.replace(/^\./, ''),
99
+ language: EXT_LANGUAGE[ext] || ext.replace(/^\./, ''),
100
+ kind: inferKind(rel),
101
+ loc: content.split(/\r?\n/).length,
102
+ bytes: stat.size,
103
+ imports: importsFrom(content),
104
+ summary: summarize(content),
105
+ content,
106
+ });
107
+ }
108
+
109
+ if (!rows.length) throw new Error(`No source files found in ${root}`);
110
+ fs.mkdirSync(path.dirname(path.resolve(outFile)), { recursive: true });
111
+ fs.writeFileSync(outFile, stringify(rows, { header: true }));
112
+ return { root, outFile: path.resolve(outFile), count: rows.length, skipped };
113
+ }
114
+ }
@@ -0,0 +1,38 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import { CONFIG_NAME } from '../constants.js';
6
+ import { normalizeBaseUrl, mcpUrl } from '../utils/url.js';
7
+
8
+ export { normalizeBaseUrl, mcpUrl };
9
+
10
+ export class FileConfigStore {
11
+ configPath() {
12
+ return path.join(os.homedir(), '.mantis', CONFIG_NAME);
13
+ }
14
+
15
+ load() {
16
+ const file = this.configPath();
17
+ if (!fs.existsSync(file)) return {};
18
+ try {
19
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
20
+ } catch {
21
+ return {};
22
+ }
23
+ }
24
+
25
+ save(cfg) {
26
+ const file = this.configPath();
27
+ fs.mkdirSync(path.dirname(file), { recursive: true });
28
+ fs.writeFileSync(file, `${JSON.stringify(cfg, null, 2)}\n`);
29
+ }
30
+
31
+ requireAuth() {
32
+ const cfg = this.load();
33
+ if (!cfg.apiKey || !cfg.apiBaseUrl) {
34
+ throw new Error('Run mantis setup first (API key + URL).');
35
+ }
36
+ return cfg;
37
+ }
38
+ }
@@ -0,0 +1,12 @@
1
+ import fs from 'node:fs';
2
+ import { parse } from 'csv-parse/sync';
3
+
4
+ export class FsCsvReader {
5
+ readHeaders(file) {
6
+ const input = fs.readFileSync(file, 'utf8');
7
+ const rows = parse(input, { to_line: 1, relax_quotes: true });
8
+ const headers = rows[0] || [];
9
+ if (!headers.length) throw new Error('CSV has no header row.');
10
+ return headers.map((h) => String(h).trim()).filter(Boolean);
11
+ }
12
+ }
@@ -0,0 +1,108 @@
1
+ import { normalizeBaseUrl } from '../utils/url.js';
2
+
3
+ function formatApiError(data, res) {
4
+ const raw = data.error || data.detail || res.statusText || `HTTP ${res.status}`;
5
+ if (typeof raw === 'string' && /<html/i.test(raw)) {
6
+ if (res.status >= 500) {
7
+ return 'Mantis server error (HTTP 500). If creating a thread, that name may already exist in this space.';
8
+ }
9
+ return `Mantis API error (HTTP ${res.status})`;
10
+ }
11
+ return typeof raw === 'string' ? raw : JSON.stringify(raw);
12
+ }
13
+
14
+ export class HttpMantisClient {
15
+ constructor(configStore) {
16
+ this.configStore = configStore;
17
+ }
18
+
19
+ _credentials() {
20
+ return this.configStore.requireAuth();
21
+ }
22
+
23
+ async request(method, pathname, { params, body } = {}) {
24
+ const { apiBaseUrl, apiKey } = this._credentials();
25
+ const root = normalizeBaseUrl(apiBaseUrl);
26
+ const url = new URL(pathname.startsWith('/') ? pathname : `/${pathname}`, `${root}/`);
27
+ if (params) {
28
+ for (const [k, v] of Object.entries(params)) {
29
+ if (v != null && v !== '') url.searchParams.set(k, String(v));
30
+ }
31
+ }
32
+ const res = await fetch(url, {
33
+ method,
34
+ headers: {
35
+ Authorization: `Bearer ${apiKey}`,
36
+ Accept: 'application/json',
37
+ ...(body ? { 'Content-Type': 'application/json' } : {}),
38
+ },
39
+ body: body ? JSON.stringify(body) : undefined,
40
+ });
41
+ const text = await res.text();
42
+ let data;
43
+ try {
44
+ data = text ? JSON.parse(text) : {};
45
+ } catch {
46
+ data = { error: text || res.statusText };
47
+ }
48
+ if (!res.ok) {
49
+ throw new Error(formatApiError(data, res));
50
+ }
51
+ return data;
52
+ }
53
+
54
+ listSpaces({ scope = 'accessible', limit = 20, offset = 0, q, space_id } = {}) {
55
+ return this.request('GET', '/api/v1/me/spaces/', {
56
+ params: { scope, limit, offset, q, space_id },
57
+ });
58
+ }
59
+
60
+ listSpaceStates(spaceId, { limit = 20, offset = 0 } = {}) {
61
+ return this.request('GET', '/api/v1/me/space-states/', {
62
+ params: { space_id: spaceId, limit, offset },
63
+ });
64
+ }
65
+
66
+ createSpaceState(spaceId, name) {
67
+ return this.request('POST', '/api/v1/me/space-states/', {
68
+ body: { space_id: spaceId, name },
69
+ });
70
+ }
71
+
72
+ createSpace({ name, isPublic = false }) {
73
+ return this.request('POST', '/api/v1/spaces/', {
74
+ body: { name, public: Boolean(isPublic) },
75
+ });
76
+ }
77
+
78
+ async createMapInSpace(spaceId, { file, mapName, dataTypes, selectedFields, fieldWeights }) {
79
+ const { apiBaseUrl, apiKey } = this._credentials();
80
+ const root = normalizeBaseUrl(apiBaseUrl);
81
+ const url = new URL(`/api/v1/spaces/${encodeURIComponent(spaceId)}/maps/`, `${root}/`);
82
+ const form = new FormData();
83
+ const bytes = await import('node:fs/promises').then((fs) => fs.readFile(file));
84
+ const name = await import('node:path').then((p) => p.basename(file));
85
+ form.set('file', new Blob([bytes]), name);
86
+ if (mapName) form.set('map_name', mapName);
87
+ if (dataTypes) form.set('data_types', JSON.stringify(dataTypes));
88
+ if (selectedFields) form.set('selected_fields', JSON.stringify(selectedFields));
89
+ if (fieldWeights) form.set('field_weights', JSON.stringify(fieldWeights));
90
+
91
+ const res = await fetch(url, {
92
+ method: 'POST',
93
+ headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' },
94
+ body: form,
95
+ });
96
+ const text = await res.text();
97
+ let data;
98
+ try {
99
+ data = text ? JSON.parse(text) : {};
100
+ } catch {
101
+ data = { error: text || res.statusText };
102
+ }
103
+ if (!res.ok) {
104
+ throw new Error(formatApiError(data, res));
105
+ }
106
+ return data;
107
+ }
108
+ }
@@ -0,0 +1,73 @@
1
+ import { confirm, input, password, search, select } from '@inquirer/prompts';
2
+
3
+ export class InquirerUiService {
4
+ isCancel(err) {
5
+ return err?.name === 'ExitPromptError';
6
+ }
7
+
8
+ die(msg) {
9
+ console.error(`\n ✖ ${msg}\n`);
10
+ process.exit(1);
11
+ }
12
+
13
+ banner(title, subtitle) {
14
+ console.log('');
15
+ console.log(` \x1b[36m${title}\x1b[0m`);
16
+ if (subtitle) console.log(` \x1b[2m${subtitle}\x1b[0m`);
17
+ console.log('');
18
+ }
19
+
20
+ success(msg) {
21
+ console.log(` \x1b[32m✔\x1b[0m ${msg}`);
22
+ }
23
+
24
+ info(msg) {
25
+ console.log(` \x1b[2m→\x1b[0m ${msg}`);
26
+ }
27
+
28
+ async promptInput(message, { default: def } = {}) {
29
+ try {
30
+ return await input({ message, default: def });
31
+ } catch (e) {
32
+ if (this.isCancel(e)) this.die('Cancelled.');
33
+ throw e;
34
+ }
35
+ }
36
+
37
+ async promptSecret(message, { default: def } = {}) {
38
+ if (def) return this.promptInput(message, { default: def });
39
+ try {
40
+ return await password({ message, mask: '•' });
41
+ } catch (e) {
42
+ if (this.isCancel(e)) this.die('Cancelled.');
43
+ throw e;
44
+ }
45
+ }
46
+
47
+ async promptSearch(message, source, { pageSize = 12 } = {}) {
48
+ try {
49
+ return await search({ message, pageSize, source });
50
+ } catch (e) {
51
+ if (this.isCancel(e)) this.die('Cancelled.');
52
+ throw e;
53
+ }
54
+ }
55
+
56
+ async promptSelect(message, choices) {
57
+ try {
58
+ return await select({ message, choices });
59
+ } catch (e) {
60
+ if (this.isCancel(e)) this.die('Cancelled.');
61
+ throw e;
62
+ }
63
+ }
64
+
65
+ async promptConfirm(message, { default: def = false } = {}) {
66
+ try {
67
+ return await confirm({ message, default: def });
68
+ } catch (e) {
69
+ if (this.isCancel(e)) this.die('Cancelled.');
70
+ throw e;
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,64 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
+
4
+ import { mcpUrl } from '../utils/url.js';
5
+
6
+ export class McpClientService {
7
+ constructor(configStore) {
8
+ this.configStore = configStore;
9
+ }
10
+
11
+ _headers(cfg) {
12
+ return {
13
+ Authorization: `Bearer ${cfg.apiKey}`,
14
+ Accept: 'application/json',
15
+ ...(cfg.spaceStateId ? { 'X-Space-State-ID': String(cfg.spaceStateId) } : {}),
16
+ };
17
+ }
18
+
19
+ async _withClient(fn) {
20
+ const cfg = this.configStore.requireAuth();
21
+ if (!cfg.spaceStateId) {
22
+ throw new Error('No thread configured. Run: mantis setup or mantis select thread');
23
+ }
24
+ const transport = new StreamableHTTPClientTransport(new URL(mcpUrl(cfg)), {
25
+ requestInit: { headers: this._headers(cfg) },
26
+ });
27
+ const client = new Client({ name: 'mantisai-cli', version: '3.0.0' });
28
+ await client.connect(transport);
29
+ try {
30
+ return await fn(client);
31
+ } finally {
32
+ await client.close();
33
+ }
34
+ }
35
+
36
+ async listTools() {
37
+ const result = await this._withClient((client) => client.listTools());
38
+ return {
39
+ tools: (result.tools || []).map((t) => ({
40
+ name: t.name,
41
+ description: t.description,
42
+ inputSchema: t.inputSchema,
43
+ })),
44
+ };
45
+ }
46
+
47
+ async callTool(name, args = {}) {
48
+ const result = await this._withClient((client) => client.callTool({ name, arguments: args }));
49
+ if (result.isError) {
50
+ const msg = result.content?.find((c) => c.type === 'text')?.text || 'Tool call failed';
51
+ throw new Error(msg);
52
+ }
53
+ if (result.structuredContent) return result.structuredContent;
54
+ const text = result.content?.find((c) => c.type === 'text')?.text;
55
+ if (text) {
56
+ try {
57
+ return JSON.parse(text);
58
+ } catch {
59
+ return { text };
60
+ }
61
+ }
62
+ return result;
63
+ }
64
+ }
@@ -0,0 +1,17 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+
4
+ import { SKILLS_DIR } from '../utils/package-root.js';
5
+ import { syncSkills } from '../utils/skills-sync.js';
6
+
7
+ export class OpencodeSkillsService {
8
+ sync(cwd = process.cwd()) {
9
+ const globalSkillsDir = path.join(os.homedir(), '.config', 'opencode', 'skills');
10
+ const projectSkillsDir = path.join(cwd, '.opencode', 'skills');
11
+ const installed = syncSkills({
12
+ skillsDir: SKILLS_DIR,
13
+ targets: [globalSkillsDir, projectSkillsDir],
14
+ });
15
+ return { globalSkillsDir, projectSkillsDir, installed };
16
+ }
17
+ }
@@ -0,0 +1,72 @@
1
+ import { API_PAGE } from '../constants.js';
2
+ import { parseSpaceIdFromInput } from '../utils/space-id.js';
3
+
4
+ export class SpaceRepositoryImpl {
5
+ constructor(client) {
6
+ this.client = client;
7
+ }
8
+
9
+ async _paginate(fetchPage) {
10
+ const all = [];
11
+ let offset = 0;
12
+ for (;;) {
13
+ const { items, total } = await fetchPage(API_PAGE, offset);
14
+ const batch = items || [];
15
+ all.push(...batch);
16
+ if (batch.length < API_PAGE) break;
17
+ if (Number.isFinite(total) && all.length >= total) break;
18
+ offset += API_PAGE;
19
+ }
20
+ return all;
21
+ }
22
+
23
+ async fetchById(spaceId) {
24
+ const page = await this.client.listSpaces({
25
+ scope: 'accessible',
26
+ space_id: spaceId,
27
+ limit: 1,
28
+ offset: 0,
29
+ });
30
+ return page.spaces?.[0] ?? null;
31
+ }
32
+
33
+ async resolveFromInput(text) {
34
+ const id = parseSpaceIdFromInput(text);
35
+ if (!id) return null;
36
+ return this.fetchById(id);
37
+ }
38
+
39
+ async search({ q = '', limit = 20, offset = 0, scope = 'accessible' } = {}) {
40
+ const id = parseSpaceIdFromInput(q);
41
+ if (id) {
42
+ const one = await this.fetchById(id);
43
+ return { spaces: one ? [one] : [], total: one ? 1 : 0, limit: 1, offset: 0 };
44
+ }
45
+ const page = await this.client.listSpaces({
46
+ scope,
47
+ limit,
48
+ offset,
49
+ q: q.trim() || undefined,
50
+ });
51
+ return {
52
+ spaces: page.spaces || [],
53
+ total: page.total ?? (page.spaces || []).length,
54
+ limit: page.limit ?? limit,
55
+ offset: page.offset ?? offset,
56
+ };
57
+ }
58
+
59
+ fetchAccessible() {
60
+ return this._paginate(async (limit, offset) => {
61
+ const page = await this.client.listSpaces({ scope: 'accessible', limit, offset });
62
+ return { items: page.spaces, total: page.total };
63
+ });
64
+ }
65
+
66
+ fetchThreads(spaceId) {
67
+ return this._paginate(async (limit, offset) => {
68
+ const page = await this.client.listSpaceStates(spaceId, { limit, offset });
69
+ return { items: page.space_states, total: page.total };
70
+ });
71
+ }
72
+ }
@@ -0,0 +1,5 @@
1
+ /** @typedef {Object} CodebaseIndexer
2
+ * @property {(rootDir: string, outFile: string, opts?: object) => Promise<object>} index
3
+ */
4
+
5
+ export {};
@@ -0,0 +1,11 @@
1
+ /** @typedef {import('../types.js').MantisConfig} MantisConfig */
2
+
3
+ /** @typedef {Object} ConfigStore
4
+ * @property {() => MantisConfig} load
5
+ * @property {(cfg: MantisConfig) => void} save
6
+ * @property {() => string} configPath
7
+ * @property {() => string[]} allConfigPaths
8
+ * @property {() => MantisConfig} requireAuth
9
+ */
10
+
11
+ export {};
@@ -0,0 +1,5 @@
1
+ /** @typedef {Object} CsvReader
2
+ * @property {(file: string) => string[]} readHeaders
3
+ */
4
+
5
+ export {};
@@ -0,0 +1,7 @@
1
+ export * from './config-store.js';
2
+ export * from './mantis-client.js';
3
+ export * from './mcp-client.js';
4
+ export * from './space-repository.js';
5
+ export * from './ui.js';
6
+ export * from './codebase-indexer.js';
7
+ export * from './csv-reader.js';
@@ -0,0 +1,13 @@
1
+ /** @typedef {import('../types.js').Space} Space */
2
+ /** @typedef {import('../types.js').Thread} Thread */
3
+ /** @typedef {import('../types.js').SpacePage} SpacePage */
4
+
5
+ /** @typedef {Object} MantisClient
6
+ * @property {(opts?: object) => Promise<SpacePage>} listSpaces
7
+ * @property {(spaceId: string, opts?: object) => Promise<{ space_states: Thread[], total: number }>} listSpaceStates
8
+ * @property {(spaceId: string, name?: string) => Promise<Thread>} createSpaceState
9
+ * @property {(opts: { name: string, isPublic?: boolean }) => Promise<Space>} createSpace
10
+ * @property {(spaceId: string, opts: object) => Promise<object>} createMapInSpace
11
+ */
12
+
13
+ export {};
@@ -0,0 +1,6 @@
1
+ /** @typedef {Object} McpClientService
2
+ * @property {() => Promise<{ tools: object[] }>} listTools
3
+ * @property {(name: string, args?: object) => Promise<object>} callTool
4
+ */
5
+
6
+ export {};
@@ -0,0 +1,13 @@
1
+ /** @typedef {import('../types.js').Space} Space */
2
+ /** @typedef {import('../types.js').SpacePage} SpacePage */
3
+ /** @typedef {import('../types.js').Thread} Thread */
4
+
5
+ /** @typedef {Object} SpaceRepository
6
+ * @property {(spaceId: string) => Promise<Space|null>} fetchById
7
+ * @property {(text: string) => Promise<Space|null>} resolveFromInput
8
+ * @property {(opts?: object) => Promise<SpacePage>} search
9
+ * @property {() => Promise<Space[]>} fetchAccessible
10
+ * @property {(spaceId: string) => Promise<Thread[]>} fetchThreads
11
+ */
12
+
13
+ export {};
@@ -0,0 +1,13 @@
1
+ /** @typedef {Object} UiService
2
+ * @property {(msg: string) => never} die
3
+ * @property {(title: string, subtitle?: string) => void} banner
4
+ * @property {(msg: string) => void} success
5
+ * @property {(msg: string) => void} info
6
+ * @property {(message: string, opts?: object) => Promise<string>} promptInput
7
+ * @property {(message: string, opts?: object) => Promise<string>} promptSecret
8
+ * @property {(message: string, source: Function, opts?: object) => Promise<any>} promptSearch
9
+ * @property {(message: string, choices: object[]) => Promise<any>} promptSelect
10
+ * @property {(message: string, opts?: object) => Promise<boolean>} promptConfirm
11
+ */
12
+
13
+ export {};