@geminilight/mindos 0.6.23 → 0.6.25

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 (36) hide show
  1. package/app/app/.well-known/agent-card.json/route.ts +34 -0
  2. package/app/app/api/a2a/route.ts +100 -0
  3. package/app/components/Backlinks.tsx +2 -2
  4. package/app/components/Breadcrumb.tsx +1 -1
  5. package/app/components/CsvView.tsx +41 -19
  6. package/app/components/DirView.tsx +2 -2
  7. package/app/components/GuideCard.tsx +6 -2
  8. package/app/components/HomeContent.tsx +1 -1
  9. package/app/components/SearchModal.tsx +3 -3
  10. package/app/components/SyncStatusBar.tsx +2 -2
  11. package/app/components/ask/AskContent.tsx +1 -1
  12. package/app/components/ask/MentionPopover.tsx +2 -2
  13. package/app/components/ask/SlashCommandPopover.tsx +1 -1
  14. package/app/components/explore/UseCaseCard.tsx +2 -2
  15. package/app/components/help/HelpContent.tsx +6 -1
  16. package/app/components/panels/AgentsPanelAgentDetail.tsx +2 -2
  17. package/app/components/panels/DiscoverPanel.tsx +3 -3
  18. package/app/components/panels/PanelNavRow.tsx +2 -2
  19. package/app/components/panels/PluginsPanel.tsx +1 -1
  20. package/app/components/panels/SearchPanel.tsx +3 -3
  21. package/app/components/renderers/summary/SummaryRenderer.tsx +1 -1
  22. package/app/components/settings/AiTab.tsx +4 -4
  23. package/app/components/settings/KnowledgeTab.tsx +1 -1
  24. package/app/components/settings/McpTab.tsx +22 -4
  25. package/app/components/settings/UpdateTab.tsx +1 -1
  26. package/app/components/setup/index.tsx +9 -3
  27. package/app/components/walkthrough/WalkthroughProvider.tsx +2 -2
  28. package/app/lib/a2a/agent-card.ts +107 -0
  29. package/app/lib/a2a/index.ts +23 -0
  30. package/app/lib/a2a/task-handler.ts +228 -0
  31. package/app/lib/a2a/types.ts +158 -0
  32. package/bin/cli.js +10 -0
  33. package/bin/commands/agent.js +18 -0
  34. package/bin/commands/api.js +58 -0
  35. package/bin/commands/search.js +51 -0
  36. package/package.json +1 -1
@@ -42,7 +42,7 @@ export default function WalkthroughProvider({ children }: WalkthroughProviderPro
42
42
  walkthroughDismissed: dismissed,
43
43
  },
44
44
  }),
45
- }).catch(() => {});
45
+ }).catch((err) => { console.warn("[WalkthroughProvider] localStorage setItem failed:", err); });
46
46
  }, []);
47
47
 
48
48
  // Check for auto-start via ?welcome=1 or guideState
@@ -81,7 +81,7 @@ export default function WalkthroughProvider({ children }: WalkthroughProviderPro
81
81
  setCurrentStep(gs.walkthroughStep);
82
82
  }
83
83
  })
84
- .catch(() => {});
84
+ .catch((err) => { console.warn("[WalkthroughProvider] guideState read failed:", err); });
85
85
  }, [totalSteps]);
86
86
 
87
87
  const start = useCallback(() => {
@@ -0,0 +1,107 @@
1
+ /**
2
+ * A2A Agent Card generator for MindOS.
3
+ * Builds the card dynamically from current settings and MCP tools.
4
+ */
5
+
6
+ import type { AgentCard, AgentSkill } from './types';
7
+
8
+ /** MindOS knowledge-base skills exposed via A2A */
9
+ const KB_SKILLS: AgentSkill[] = [
10
+ {
11
+ id: 'kb-search',
12
+ name: 'Search Knowledge Base',
13
+ description: 'Full-text search across all notes, files, and spaces in the knowledge base.',
14
+ tags: ['search', 'knowledge', 'notes'],
15
+ examples: ['Search for notes about machine learning', 'Find files mentioning project deadlines'],
16
+ inputModes: ['text/plain'],
17
+ outputModes: ['text/plain', 'application/json'],
18
+ },
19
+ {
20
+ id: 'kb-read',
21
+ name: 'Read Note',
22
+ description: 'Read the full content of a specific file in the knowledge base.',
23
+ tags: ['read', 'file', 'content'],
24
+ examples: ['Read the file at Projects/roadmap.md'],
25
+ inputModes: ['text/plain'],
26
+ outputModes: ['text/plain'],
27
+ },
28
+ {
29
+ id: 'kb-write',
30
+ name: 'Write Note',
31
+ description: 'Create or update a note in the knowledge base. Supports .md and .csv files.',
32
+ tags: ['write', 'create', 'update'],
33
+ examples: ['Create a new meeting note at Work/meetings/2026-03-30.md'],
34
+ inputModes: ['text/plain'],
35
+ outputModes: ['text/plain'],
36
+ },
37
+ {
38
+ id: 'kb-list',
39
+ name: 'List Files',
40
+ description: 'List files and directory structure of the knowledge base.',
41
+ tags: ['list', 'tree', 'structure'],
42
+ examples: ['Show the file tree', 'List all spaces'],
43
+ inputModes: ['text/plain'],
44
+ outputModes: ['application/json'],
45
+ },
46
+ {
47
+ id: 'kb-organize',
48
+ name: 'Organize Files',
49
+ description: 'AI-powered file organization into appropriate spaces and directories.',
50
+ tags: ['organize', 'ai', 'structure'],
51
+ examples: ['Organize imported files into relevant spaces'],
52
+ inputModes: ['text/plain'],
53
+ outputModes: ['text/plain'],
54
+ },
55
+ ];
56
+
57
+ /**
58
+ * Build the A2A Agent Card for this MindOS instance.
59
+ * @param baseUrl The publicly reachable base URL (e.g. http://localhost:3456)
60
+ */
61
+ export function buildAgentCard(baseUrl: string): AgentCard {
62
+ let version = process.env.npm_package_version || '0.0.0';
63
+
64
+ // Try reading version from package.json as fallback
65
+ if (version === '0.0.0') {
66
+ try {
67
+ const fs = require('fs');
68
+ const path = require('path');
69
+ const projRoot = process.env.MINDOS_PROJECT_ROOT || path.resolve(process.cwd(), '..');
70
+ const pkg = JSON.parse(fs.readFileSync(path.join(projRoot, 'package.json'), 'utf-8'));
71
+ if (pkg.version) version = pkg.version;
72
+ } catch { /* use default */ }
73
+ }
74
+
75
+ return {
76
+ name: 'MindOS',
77
+ description: 'Personal knowledge management system with AI-powered Spaces, Instructions, and Skills. Store, organize, and retrieve knowledge through natural language.',
78
+ version,
79
+ provider: {
80
+ organization: 'MindOS',
81
+ url: baseUrl,
82
+ },
83
+ supportedInterfaces: [
84
+ {
85
+ url: `${baseUrl}/api/a2a`,
86
+ protocolBinding: 'JSONRPC',
87
+ protocolVersion: '1.0',
88
+ },
89
+ ],
90
+ capabilities: {
91
+ streaming: false, // Phase 1: no streaming
92
+ pushNotifications: false,
93
+ stateTransitionHistory: false,
94
+ },
95
+ defaultInputModes: ['text/plain'],
96
+ defaultOutputModes: ['text/plain', 'application/json'],
97
+ skills: KB_SKILLS,
98
+ securitySchemes: {
99
+ bearer: {
100
+ httpAuthSecurityScheme: {
101
+ scheme: 'Bearer',
102
+ },
103
+ },
104
+ },
105
+ securityRequirements: [{ bearer: [] }],
106
+ };
107
+ }
@@ -0,0 +1,23 @@
1
+ export { buildAgentCard } from './agent-card';
2
+ export { handleSendMessage, handleGetTask, handleCancelTask } from './task-handler';
3
+ export { A2A_ERRORS } from './types';
4
+ export type {
5
+ AgentCard,
6
+ AgentInterface,
7
+ AgentCapabilities,
8
+ AgentSkill,
9
+ SecurityScheme,
10
+ JsonRpcRequest,
11
+ JsonRpcResponse,
12
+ JsonRpcError,
13
+ TaskState,
14
+ MessageRole,
15
+ MessagePart,
16
+ A2AMessage,
17
+ TaskStatus,
18
+ TaskArtifact,
19
+ A2ATask,
20
+ SendMessageParams,
21
+ GetTaskParams,
22
+ CancelTaskParams,
23
+ } from './types';
@@ -0,0 +1,228 @@
1
+ /**
2
+ * A2A Task handler for MindOS.
3
+ * Routes A2A SendMessage requests to internal MCP tools.
4
+ */
5
+
6
+ import { randomUUID } from 'crypto';
7
+ import type {
8
+ A2ATask,
9
+ A2AMessage,
10
+ SendMessageParams,
11
+ GetTaskParams,
12
+ CancelTaskParams,
13
+ TaskState,
14
+ } from './types';
15
+
16
+ /* ── In-memory Task Store (Phase 1) ───────────────────────────────────── */
17
+ // NOTE: In-memory Map is lost on serverless cold starts / process restarts.
18
+ // Acceptable for Phase 1. Phase 2 should use persistent storage if needed.
19
+
20
+ const tasks = new Map<string, A2ATask>();
21
+ const MAX_TASKS = 1000;
22
+
23
+ function pruneOldTasks() {
24
+ if (tasks.size <= MAX_TASKS) return;
25
+ // Remove oldest completed tasks first
26
+ const entries = [...tasks.entries()].sort((a, b) =>
27
+ new Date(a[1].status.timestamp).getTime() - new Date(b[1].status.timestamp).getTime()
28
+ );
29
+ const toRemove = entries.slice(0, tasks.size - MAX_TASKS);
30
+ for (const [id] of toRemove) tasks.delete(id);
31
+ }
32
+
33
+ /* ── Skill Router ─────────────────────────────────────────────────────── */
34
+
35
+ interface SkillRoute {
36
+ pattern: RegExp;
37
+ tool: string;
38
+ extractParams: (text: string) => Record<string, string>;
39
+ }
40
+
41
+ const SKILL_ROUTES: SkillRoute[] = [
42
+ {
43
+ pattern: /^(?:search|find|look\s*up|query)\b/i,
44
+ tool: 'search_notes',
45
+ extractParams: (text) => ({ q: text.replace(/^(?:search|find|look\s*up|query)\s+(?:for\s+)?/i, '').trim() }),
46
+ },
47
+ {
48
+ pattern: /^(?:read|get|show|open|view)\s+(?:the\s+)?(?:file\s+)?(?:at\s+)?(.+\.(?:md|csv))/i,
49
+ tool: 'read_file',
50
+ extractParams: (text) => {
51
+ const match = text.match(/(?:at\s+)?([^\s]+\.(?:md|csv))/i);
52
+ return { path: match?.[1] ?? '' };
53
+ },
54
+ },
55
+ {
56
+ pattern: /^(?:list|show|tree)\s+(?:files|spaces|structure)/i,
57
+ tool: 'list_files',
58
+ extractParams: () => ({}),
59
+ },
60
+ {
61
+ pattern: /^(?:list|show)\s+spaces/i,
62
+ tool: 'list_spaces',
63
+ extractParams: () => ({}),
64
+ },
65
+ ];
66
+
67
+ function routeToTool(text: string): { tool: string; params: Record<string, string> } | null {
68
+ for (const route of SKILL_ROUTES) {
69
+ if (route.pattern.test(text)) {
70
+ return { tool: route.tool, params: route.extractParams(text) };
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+
76
+ /* ── Execute via internal API ─────────────────────────────────────────── */
77
+
78
+ const TOOL_TIMEOUT_MS = 10_000;
79
+
80
+ async function fetchWithTimeout(url: string): Promise<Response> {
81
+ const controller = new AbortController();
82
+ const timeout = setTimeout(() => controller.abort(), TOOL_TIMEOUT_MS);
83
+ try {
84
+ const res = await fetch(url, { signal: controller.signal });
85
+ return res;
86
+ } finally {
87
+ clearTimeout(timeout);
88
+ }
89
+ }
90
+
91
+ /** Sanitize file path: reject traversal attempts */
92
+ function sanitizePath(p: string): string {
93
+ if (!p || p.includes('..') || p.includes('\0')) throw new Error('Invalid path');
94
+ // Normalize double slashes and strip leading slashes
95
+ return p.replace(/\/\//g, '/').replace(/^\/+/, '');
96
+ }
97
+
98
+ async function executeTool(tool: string, params: Record<string, string>): Promise<string> {
99
+ const baseUrl = `http://localhost:${process.env.PORT || 3456}`;
100
+
101
+ switch (tool) {
102
+ case 'search_notes': {
103
+ const q = (params.q || '').slice(0, 500); // limit query length
104
+ const res = await fetchWithTimeout(`${baseUrl}/api/search?q=${encodeURIComponent(q)}`);
105
+ if (!res.ok) throw new Error(`Search failed: ${res.status}`);
106
+ const data = await res.json();
107
+ return JSON.stringify(data, null, 2);
108
+ }
109
+ case 'read_file': {
110
+ const safePath = sanitizePath(params.path || '');
111
+ const res = await fetchWithTimeout(`${baseUrl}/api/file?path=${encodeURIComponent(safePath)}`);
112
+ if (!res.ok) throw new Error(`Read failed: ${res.status}`);
113
+ const data = await res.json();
114
+ return typeof data.content === 'string' ? data.content : JSON.stringify(data);
115
+ }
116
+ case 'list_files': {
117
+ const res = await fetchWithTimeout(`${baseUrl}/api/files`);
118
+ if (!res.ok) throw new Error(`List failed: ${res.status}`);
119
+ const data = await res.json();
120
+ return JSON.stringify(data, null, 2);
121
+ }
122
+ case 'list_spaces': {
123
+ const res = await fetchWithTimeout(`${baseUrl}/api/files`);
124
+ if (!res.ok) throw new Error(`List failed: ${res.status}`);
125
+ const data = await res.json();
126
+ const spaces = (data.tree ?? data.files ?? []).filter((n: { isSpace?: boolean }) => n.isSpace);
127
+ return JSON.stringify(spaces, null, 2);
128
+ }
129
+ default:
130
+ throw new Error(`Unknown tool: ${tool}`);
131
+ }
132
+ }
133
+
134
+ /* ── Public API ────────────────────────────────────────────────────────── */
135
+
136
+ export async function handleSendMessage(params: SendMessageParams): Promise<A2ATask> {
137
+ const taskId = randomUUID();
138
+ const now = new Date().toISOString();
139
+
140
+ // Extract text from message parts
141
+ const text = params.message.parts
142
+ .map(p => p.text ?? (p.data ? JSON.stringify(p.data) : ''))
143
+ .join(' ')
144
+ .trim();
145
+
146
+ if (!text) {
147
+ const failedTask = createTask(taskId, 'TASK_STATE_FAILED', 'Empty message — no text content found.', now);
148
+ tasks.set(taskId, failedTask);
149
+ return failedTask;
150
+ }
151
+
152
+ // Create task in WORKING state
153
+ const task = createTask(taskId, 'TASK_STATE_WORKING', undefined, now);
154
+ task.history = [params.message];
155
+ tasks.set(taskId, task);
156
+ pruneOldTasks();
157
+
158
+ // Route to tool
159
+ const route = routeToTool(text);
160
+
161
+ try {
162
+ let result: string;
163
+ if (route) {
164
+ result = await executeTool(route.tool, route.params);
165
+ } else {
166
+ // Fallback: treat as search query
167
+ result = await executeTool('search_notes', { q: text });
168
+ }
169
+
170
+ // Update task to completed
171
+ task.status = {
172
+ state: 'TASK_STATE_COMPLETED',
173
+ timestamp: new Date().toISOString(),
174
+ };
175
+ task.artifacts = [{
176
+ artifactId: randomUUID(),
177
+ name: 'result',
178
+ parts: [{ text: result, mediaType: 'text/plain' }],
179
+ }];
180
+ task.history.push({
181
+ role: 'ROLE_AGENT',
182
+ parts: [{ text: result }],
183
+ });
184
+
185
+ return task;
186
+ } catch (err) {
187
+ task.status = {
188
+ state: 'TASK_STATE_FAILED',
189
+ message: {
190
+ role: 'ROLE_AGENT',
191
+ parts: [{ text: `Error: ${(err as Error).message}` }],
192
+ },
193
+ timestamp: new Date().toISOString(),
194
+ };
195
+ return task;
196
+ }
197
+ }
198
+
199
+ export function handleGetTask(params: GetTaskParams): A2ATask | null {
200
+ return tasks.get(params.id) ?? null;
201
+ }
202
+
203
+ export function handleCancelTask(params: CancelTaskParams): A2ATask | null {
204
+ const task = tasks.get(params.id);
205
+ if (!task) return null;
206
+
207
+ const terminalStates: TaskState[] = ['TASK_STATE_COMPLETED', 'TASK_STATE_FAILED', 'TASK_STATE_CANCELED', 'TASK_STATE_REJECTED'];
208
+ if (terminalStates.includes(task.status.state)) return null; // not cancelable
209
+
210
+ task.status = {
211
+ state: 'TASK_STATE_CANCELED',
212
+ timestamp: new Date().toISOString(),
213
+ };
214
+ return task;
215
+ }
216
+
217
+ /* ── Helpers ───────────────────────────────────────────────────────────── */
218
+
219
+ function createTask(id: string, state: TaskState, errorMessage: string | undefined, timestamp: string): A2ATask {
220
+ return {
221
+ id,
222
+ status: {
223
+ state,
224
+ timestamp,
225
+ ...(errorMessage ? { message: { role: 'ROLE_AGENT', parts: [{ text: errorMessage }] } } : {}),
226
+ },
227
+ };
228
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * A2A Protocol v1.0 — Core Types for MindOS Agent Card & Task handling.
3
+ * Subset of the full spec: only what's needed for Phase 1 (Server mode).
4
+ * Reference: https://a2a-protocol.org/latest/specification/
5
+ */
6
+
7
+ /* ── Agent Card ────────────────────────────────────────────────────────── */
8
+
9
+ export interface AgentCard {
10
+ name: string;
11
+ description: string;
12
+ version: string;
13
+ provider: {
14
+ organization: string;
15
+ url: string;
16
+ };
17
+ supportedInterfaces: AgentInterface[];
18
+ capabilities: AgentCapabilities;
19
+ defaultInputModes: string[];
20
+ defaultOutputModes: string[];
21
+ skills: AgentSkill[];
22
+ securitySchemes?: Record<string, SecurityScheme>;
23
+ securityRequirements?: Record<string, string[]>[];
24
+ }
25
+
26
+ export interface AgentInterface {
27
+ url: string;
28
+ protocolBinding: 'JSONRPC' | 'GRPC' | 'HTTP_JSON';
29
+ protocolVersion: string;
30
+ }
31
+
32
+ export interface AgentCapabilities {
33
+ streaming: boolean;
34
+ pushNotifications: boolean;
35
+ stateTransitionHistory: boolean;
36
+ }
37
+
38
+ export interface AgentSkill {
39
+ id: string;
40
+ name: string;
41
+ description: string;
42
+ tags?: string[];
43
+ examples?: string[];
44
+ inputModes?: string[];
45
+ outputModes?: string[];
46
+ }
47
+
48
+ export interface SecurityScheme {
49
+ httpAuthSecurityScheme?: {
50
+ scheme: string;
51
+ bearerFormat?: string;
52
+ };
53
+ }
54
+
55
+ /* ── JSON-RPC ──────────────────────────────────────────────────────────── */
56
+
57
+ export interface JsonRpcRequest {
58
+ jsonrpc: '2.0';
59
+ id: string | number;
60
+ method: string;
61
+ params?: Record<string, unknown>;
62
+ }
63
+
64
+ export interface JsonRpcResponse {
65
+ jsonrpc: '2.0';
66
+ id: string | number | null;
67
+ result?: unknown;
68
+ error?: JsonRpcError;
69
+ }
70
+
71
+ export interface JsonRpcError {
72
+ code: number;
73
+ message: string;
74
+ data?: unknown;
75
+ }
76
+
77
+ /* ── A2A Messages & Tasks ──────────────────────────────────────────────── */
78
+
79
+ export type TaskState =
80
+ | 'TASK_STATE_SUBMITTED'
81
+ | 'TASK_STATE_WORKING'
82
+ | 'TASK_STATE_INPUT_REQUIRED'
83
+ | 'TASK_STATE_COMPLETED'
84
+ | 'TASK_STATE_FAILED'
85
+ | 'TASK_STATE_CANCELED'
86
+ | 'TASK_STATE_REJECTED';
87
+
88
+ export type MessageRole = 'ROLE_USER' | 'ROLE_AGENT';
89
+
90
+ export interface MessagePart {
91
+ text?: string;
92
+ data?: unknown;
93
+ mediaType?: string;
94
+ metadata?: Record<string, unknown>;
95
+ }
96
+
97
+ export interface A2AMessage {
98
+ role: MessageRole;
99
+ parts: MessagePart[];
100
+ metadata?: Record<string, unknown>;
101
+ }
102
+
103
+ export interface TaskStatus {
104
+ state: TaskState;
105
+ message?: A2AMessage;
106
+ timestamp: string;
107
+ }
108
+
109
+ export interface TaskArtifact {
110
+ artifactId: string;
111
+ name?: string;
112
+ description?: string;
113
+ parts: MessagePart[];
114
+ }
115
+
116
+ export interface A2ATask {
117
+ id: string;
118
+ contextId?: string;
119
+ status: TaskStatus;
120
+ artifacts?: TaskArtifact[];
121
+ history?: A2AMessage[];
122
+ metadata?: Record<string, unknown>;
123
+ }
124
+
125
+ /* ── A2A Method Params ─────────────────────────────────────────────────── */
126
+
127
+ export interface SendMessageParams {
128
+ message: A2AMessage;
129
+ configuration?: {
130
+ acceptedOutputModes?: string[];
131
+ blocking?: boolean;
132
+ historyLength?: number;
133
+ };
134
+ metadata?: Record<string, unknown>;
135
+ }
136
+
137
+ export interface GetTaskParams {
138
+ id: string;
139
+ historyLength?: number;
140
+ }
141
+
142
+ export interface CancelTaskParams {
143
+ id: string;
144
+ }
145
+
146
+ /* ── Error Codes ───────────────────────────────────────────────────────── */
147
+
148
+ export const A2A_ERRORS = {
149
+ TASK_NOT_FOUND: { code: -32001, message: 'Task not found' },
150
+ TASK_NOT_CANCELABLE: { code: -32002, message: 'Task not cancelable' },
151
+ UNSUPPORTED_OPERATION: { code: -32004, message: 'Unsupported operation' },
152
+ CONTENT_TYPE_NOT_SUPPORTED: { code: -32005, message: 'Content type not supported' },
153
+ PARSE_ERROR: { code: -32700, message: 'Parse error' },
154
+ INVALID_REQUEST: { code: -32600, message: 'Invalid request' },
155
+ METHOD_NOT_FOUND: { code: -32601, message: 'Method not found' },
156
+ INVALID_PARAMS: { code: -32602, message: 'Invalid params' },
157
+ INTERNAL_ERROR: { code: -32603, message: 'Internal error' },
158
+ } as const;
package/bin/cli.js CHANGED
@@ -67,6 +67,9 @@ import * as fileCmd from './commands/file.js';
67
67
  import * as spaceCmd from './commands/space.js';
68
68
  import * as askCmd from './commands/ask.js';
69
69
  import * as statusCmd from './commands/status.js';
70
+ import * as apiCmd from './commands/api.js';
71
+ import * as agentCmd from './commands/agent.js';
72
+ import * as searchCmd from './commands/search.js';
70
73
 
71
74
  // ── Helpers ───────────────────────────────────────────────────────────────────
72
75
 
@@ -1311,6 +1314,9 @@ ${bold('Examples:')}
1311
1314
  space: async () => { const p = parseArgs(process.argv.slice(3)); await spaceCmd.run([p.command, ...p.args].filter(Boolean), p.flags); },
1312
1315
  ask: async () => { const p = parseArgs(process.argv.slice(3)); await askCmd.run([p.command, ...p.args].filter(Boolean), p.flags); },
1313
1316
  status: async () => { const p = parseArgs(process.argv.slice(3)); await statusCmd.run([p.command, ...p.args].filter(Boolean), p.flags); },
1317
+ api: async () => { const p = parseArgs(process.argv.slice(3)); await apiCmd.run([p.command, ...p.args].filter(Boolean), p.flags); },
1318
+ agent: async () => { const p = parseArgs(process.argv.slice(3)); await agentCmd.run([p.command, ...p.args].filter(Boolean), p.flags); },
1319
+ search: async () => { const p = parseArgs(process.argv.slice(3)); await searchCmd.run([p.command, ...p.args].filter(Boolean), p.flags); },
1314
1320
  };
1315
1321
 
1316
1322
  // ── Entry ─────────────────────────────────────────────────────────────────────
@@ -1337,7 +1343,10 @@ ${row('mindos open', 'Open Web UI in browser')}
1337
1343
  ${bold('Knowledge:')}
1338
1344
  ${row('mindos file <sub>', 'File operations (list/read/create/delete/search)')}
1339
1345
  ${row('mindos space <sub>', 'Space management (list/create/info)')}
1346
+ ${row('mindos search "<query>"', 'Search knowledge base via API')}
1340
1347
  ${row('mindos ask "<question>"', 'Ask AI using your knowledge base')}
1348
+ ${row('mindos agent <sub>', 'AI Agent management (list/info)')}
1349
+ ${row('mindos api <METHOD> <path>', 'Raw API passthrough (for developers/agents)')}
1341
1350
 
1342
1351
  ${bold('MCP:')}
1343
1352
  ${row('mindos mcp', 'Start MCP server only')}
@@ -1354,6 +1363,7 @@ ${bold('Config & Diagnostics:')}
1354
1363
  ${row('mindos config <sub>', 'View/update config (show/set/unset/validate)')}
1355
1364
  ${row('mindos doctor', 'Health check')}
1356
1365
  ${row('mindos update', 'Update to latest version')}
1366
+ ${row('mindos uninstall', 'Fully uninstall MindOS')}
1357
1367
  ${row('mindos logs', 'Tail service logs')}
1358
1368
 
1359
1369
  ${bold('Global Flags:')}
@@ -0,0 +1,18 @@
1
+ import{bold,dim,cyan,green,red,yellow}from"../lib/colors.js";import{MCP_AGENTS,detectAgentPresence}from"../lib/mcp-agents.js";import{existsSync,readFileSync}from"node:fs";import{resolve}from"node:path";import{homedir}from"node:os";import{output,isJsonMode}from"../lib/command.js";function expandHome(p){return p.startsWith("~/")?resolve(homedir(),p.slice(2)):p}export const meta={name:"agent",group:"Knowledge",summary:"AI Agent management (list/info)",usage:"mindos agent <subcommand>"};export async function run(a,f){const s=a[0];if(!s||f.help||f.h){console.log(bold("mindos agent")+" — AI Agent management
2
+
3
+ Subcommands:
4
+ list List detected AI agents
5
+ info Show agent details
6
+
7
+ Agent Keys: "+Object.keys(MCP_AGENTS).join(", "));return}if(s==="list"||s==="ls")return agentList(f);if(s==="info")return agentInfo(a[1],f);console.error(red("Unknown: "+s));process.exit(1)}function hasMindosConfig(agent){const paths=[agent.global,agent.project].filter(Boolean).map(expandHome);for(const p of paths){try{if(!existsSync(p))continue;const raw=readFileSync(p,"utf-8").replace(///.*$/gm,"").replace(//*[\s\S]*?*//g,"");const data=JSON.parse(raw);const servers=data[agent.key]||{};if(Object.keys(servers).some(k=>k.toLowerCase().includes("mindos")))return true}catch{}}return false}function agentList(f){const agents=[];for(const[key,agent]of Object.entries(MCP_AGENTS)){if(!detectAgentPresence(key))continue;agents.push({key,name:agent.name,installed:true,mindosConnected:hasMindosConfig(agent)})}if(isJsonMode(f)){output({count:agents.length,agents},f);return}if(agents.length===0){console.log(dim("No AI agents detected."));return}console.log("
8
+ "+bold("Detected Agents ("+agents.length+"):")+"
9
+ ");for(const a of agents){console.log(" "+a.name.padEnd(20)+" "+(a.mindosConnected?green("● connected"):dim("○ not connected")))}console.log("
10
+ "+dim("Connect: mindos mcp install <agent-key>")+"
11
+ ")}function agentInfo(key,f){if(!key){console.error(red("Usage: mindos agent info <key>"));process.exit(1)}const agent=MCP_AGENTS[key];if(!agent){console.error(red("Unknown: "+key));process.exit(1)}const installed=detectAgentPresence(key);const connected=installed?hasMindosConfig(agent):false;const info={key,name:agent.name,installed,mindosConnected:connected,transport:agent.preferredTransport};if(isJsonMode(f)){output(info,f);return}console.log("
12
+ "+bold(agent.name)+"
13
+ Key: "+key+"
14
+ Installed: "+(installed?green("yes"):red("no"))+"
15
+ MindOS: "+(connected?green("connected"):yellow("not connected"))+"
16
+ Transport: "+agent.preferredTransport+(agent.global?"
17
+ Config: "+expandHome(agent.global):"")+"
18
+ ")}
@@ -0,0 +1,58 @@
1
+ import { bold, dim, cyan, red } from '../lib/colors.js';
2
+ import { loadConfig } from '../lib/config.js';
3
+
4
+ export const meta = {
5
+ name: 'api',
6
+ group: 'Knowledge',
7
+ summary: 'Raw API passthrough (GET/POST/PUT/DELETE any endpoint)',
8
+ usage: 'mindos api <METHOD> <path>',
9
+ };
10
+
11
+ export async function run(args, flags) {
12
+ if (args.length < 2 || flags.help || flags.h) {
13
+ console.log(bold('mindos api') + ' — Raw API passthrough\n');
14
+ console.log('Usage: mindos api <METHOD> <path> [--body <json>]');
15
+ console.log('Methods: GET, POST, PUT, PATCH, DELETE\n');
16
+ console.log('Examples:');
17
+ console.log(' mindos api GET /api/health');
18
+ console.log(' mindos api GET /api/files');
19
+ console.log(' mindos api POST /api/ask --body \'{"question":"..."}\'');
20
+ return;
21
+ }
22
+
23
+ const method = args[0].toUpperCase();
24
+ if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
25
+ console.error(red('Invalid method: ' + args[0]));
26
+ process.exit(1);
27
+ }
28
+
29
+ let apiPath = args[1];
30
+ if (!apiPath.startsWith('/')) apiPath = '/' + apiPath;
31
+
32
+ loadConfig();
33
+ const port = flags.port || process.env.MINDOS_WEB_PORT || '3456';
34
+ const token = process.env.MINDOS_AUTH_TOKEN || '';
35
+ const headers = { 'Content-Type': 'application/json' };
36
+ if (token) headers['Authorization'] = 'Bearer ' + token;
37
+
38
+ const fetchOpts = { method, headers };
39
+ if (flags.body && method !== 'GET') fetchOpts.body = flags.body;
40
+
41
+ try {
42
+ const res = await fetch('http://localhost:' + port + apiPath, fetchOpts);
43
+ const ct = res.headers.get('content-type') || '';
44
+ if (ct.includes('json')) {
45
+ console.log(JSON.stringify(await res.json(), null, 2));
46
+ } else {
47
+ console.log(await res.text());
48
+ }
49
+ if (!res.ok) process.exit(1);
50
+ } catch (err) {
51
+ if (err.cause && err.cause.code === 'ECONNREFUSED') {
52
+ console.error(red('Connection refused. Start with: mindos start'));
53
+ } else {
54
+ console.error(red('Request failed: ' + err.message));
55
+ }
56
+ process.exit(1);
57
+ }
58
+ }