@vicoa/opencode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,335 @@
1
+ const LANGUAGE_MAP = {
2
+ py: 'python',
3
+ js: 'javascript',
4
+ ts: 'typescript',
5
+ jsx: 'jsx',
6
+ tsx: 'tsx',
7
+ java: 'java',
8
+ cpp: 'cpp',
9
+ c: 'c',
10
+ cs: 'csharp',
11
+ rb: 'ruby',
12
+ go: 'go',
13
+ rs: 'rust',
14
+ php: 'php',
15
+ swift: 'swift',
16
+ kt: 'kotlin',
17
+ yaml: 'yaml',
18
+ yml: 'yaml',
19
+ json: 'json',
20
+ xml: 'xml',
21
+ html: 'html',
22
+ css: 'css',
23
+ scss: 'scss',
24
+ sql: 'sql',
25
+ sh: 'bash',
26
+ bash: 'bash',
27
+ md: 'markdown',
28
+ txt: 'text',
29
+ };
30
+ function truncateText(text, maxLength = 100) {
31
+ if (text.length <= maxLength) {
32
+ return text;
33
+ }
34
+ return `${text.slice(0, maxLength)}...`;
35
+ }
36
+ function detectLanguage(filePath) {
37
+ const extension = filePath.includes('.') ? filePath.split('.').pop() ?? '' : '';
38
+ return LANGUAGE_MAP[extension] ?? '';
39
+ }
40
+ function getString(input, keys, fallback = '') {
41
+ for (const key of keys) {
42
+ const value = input[key];
43
+ if (typeof value === 'string') {
44
+ return value;
45
+ }
46
+ }
47
+ return fallback;
48
+ }
49
+ function getBoolean(input, keys, fallback = false) {
50
+ for (const key of keys) {
51
+ const value = input[key];
52
+ if (typeof value === 'boolean') {
53
+ return value;
54
+ }
55
+ }
56
+ return fallback;
57
+ }
58
+ function getArray(input, keys) {
59
+ for (const key of keys) {
60
+ const value = input[key];
61
+ if (Array.isArray(value)) {
62
+ return value;
63
+ }
64
+ }
65
+ return [];
66
+ }
67
+ function formatDiffBlock(oldText, newText) {
68
+ const diffLines = ['```diff'];
69
+ if (!oldText && newText) {
70
+ for (const line of newText.split('\n')) {
71
+ diffLines.push(`+ ${line}`);
72
+ }
73
+ }
74
+ else if (oldText && !newText) {
75
+ for (const line of oldText.split('\n')) {
76
+ diffLines.push(`- ${line}`);
77
+ }
78
+ }
79
+ else {
80
+ const oldLines = oldText.split('\n');
81
+ const newLines = newText.split('\n');
82
+ const commonPrefix = [];
83
+ const commonSuffix = [];
84
+ for (let i = 0; i < Math.min(oldLines.length, newLines.length); i += 1) {
85
+ if (oldLines[i] === newLines[i]) {
86
+ commonPrefix.push(oldLines[i]);
87
+ }
88
+ else {
89
+ break;
90
+ }
91
+ }
92
+ const oldRemaining = oldLines.slice(commonPrefix.length);
93
+ const newRemaining = newLines.slice(commonPrefix.length);
94
+ if (oldRemaining.length && newRemaining.length) {
95
+ for (let i = 1; i <= Math.min(oldRemaining.length, newRemaining.length); i += 1) {
96
+ if (oldRemaining[oldRemaining.length - i] === newRemaining[newRemaining.length - i]) {
97
+ commonSuffix.unshift(oldRemaining[oldRemaining.length - i]);
98
+ }
99
+ else {
100
+ break;
101
+ }
102
+ }
103
+ }
104
+ const changedOld = commonSuffix.length
105
+ ? oldRemaining.slice(0, oldRemaining.length - commonSuffix.length)
106
+ : oldRemaining;
107
+ const changedNew = commonSuffix.length
108
+ ? newRemaining.slice(0, newRemaining.length - commonSuffix.length)
109
+ : newRemaining;
110
+ if ((commonPrefix.length || commonSuffix.length) && (changedOld.length || changedNew.length)) {
111
+ const contextBefore = commonPrefix.slice(-2);
112
+ const contextAfter = commonSuffix.slice(0, 2);
113
+ for (const line of contextBefore) {
114
+ diffLines.push(` ${line}`);
115
+ }
116
+ for (const line of changedOld) {
117
+ diffLines.push(`- ${line}`);
118
+ }
119
+ for (const line of changedNew) {
120
+ diffLines.push(`+ ${line}`);
121
+ }
122
+ for (const line of contextAfter) {
123
+ diffLines.push(` ${line}`);
124
+ }
125
+ }
126
+ else {
127
+ for (const line of oldLines) {
128
+ diffLines.push(`- ${line}`);
129
+ }
130
+ for (const line of newLines) {
131
+ diffLines.push(`+ ${line}`);
132
+ }
133
+ }
134
+ }
135
+ diffLines.push('```');
136
+ return diffLines;
137
+ }
138
+ export function formatToolUsage(toolName, inputData) {
139
+ if (toolName.startsWith('mcp__vicoa__')) {
140
+ return `Using tool: ${toolName}`;
141
+ }
142
+ const normalizedTool = toolName.toLowerCase();
143
+ if (normalizedTool === 'write') {
144
+ const filePath = getString(inputData, ['file_path', 'filePath', 'path', 'filename'], 'unknown');
145
+ const content = getString(inputData, ['content', 'text', 'value']);
146
+ const lang = detectLanguage(filePath);
147
+ return [`Using tool: Write - \`${filePath}\``, `\`\`\`${lang}`, content, '```']
148
+ .filter((line) => line.length > 0)
149
+ .join('\n');
150
+ }
151
+ if (normalizedTool === 'read' || normalizedTool === 'notebookread' || normalizedTool === 'notebookedit') {
152
+ const filePath = getString(inputData, ['file_path', 'filePath', 'path', 'notebook_path'], 'unknown');
153
+ return `Using tool: ${toolName} - \`${filePath}\``;
154
+ }
155
+ if (normalizedTool === 'edit') {
156
+ const filePath = getString(inputData, ['file_path', 'filePath', 'path'], 'unknown');
157
+ const oldString = getString(inputData, ['old_string', 'oldString']);
158
+ const newString = getString(inputData, ['new_string', 'newString']);
159
+ const replaceAll = getBoolean(inputData, ['replace_all', 'replaceAll']);
160
+ const diffLines = [`Using tool: **Edit** - \`${filePath}\``];
161
+ if (replaceAll) {
162
+ diffLines.push('*Replacing all occurrences*');
163
+ }
164
+ diffLines.push('');
165
+ diffLines.push(...formatDiffBlock(oldString, newString));
166
+ return diffLines.join('\n');
167
+ }
168
+ if (normalizedTool === 'multiedit') {
169
+ const filePath = getString(inputData, ['file_path', 'filePath', 'path'], 'unknown');
170
+ const edits = getArray(inputData, ['edits']);
171
+ const lines = [
172
+ `Using tool: **MultiEdit** - \`${filePath}\``,
173
+ `*Making ${edits.length} edit${edits.length === 1 ? '' : 's'}:*`,
174
+ '',
175
+ ];
176
+ edits.forEach((edit, index) => {
177
+ const editIndex = index + 1;
178
+ const oldString = typeof edit.old_string === 'string' ? edit.old_string : edit.oldString ?? '';
179
+ const newString = typeof edit.new_string === 'string' ? edit.new_string : edit.newString ?? '';
180
+ const replaceAll = Boolean(edit.replace_all ?? edit.replaceAll ?? false);
181
+ lines.push(replaceAll ? `### Edit ${editIndex} *(replacing all occurrences)*` : `### Edit ${editIndex}`);
182
+ lines.push('');
183
+ lines.push(...formatDiffBlock(oldString, newString));
184
+ lines.push('');
185
+ });
186
+ return lines.join('\n');
187
+ }
188
+ if (normalizedTool === 'bash') {
189
+ const command = getString(inputData, ['command', 'cmd']);
190
+ return `Using tool: Bash - \`${command}\``;
191
+ }
192
+ if (normalizedTool === 'grep' || normalizedTool === 'glob') {
193
+ const pattern = getString(inputData, ['pattern', 'query'], 'unknown');
194
+ const path = getString(inputData, ['path', 'directory'], 'current directory');
195
+ return `Using tool: ${toolName} - \`${truncateText(pattern, 50)}\` in ${path}`;
196
+ }
197
+ if (normalizedTool === 'list' || normalizedTool === 'ls') {
198
+ const path = getString(inputData, ['path'], 'unknown');
199
+ return `Using tool: list - \`${path}\``;
200
+ }
201
+ if (normalizedTool === 'patch') {
202
+ const file = getString(inputData, ['file', 'path'], 'unknown');
203
+ return `Using tool: patch - \`${file}\``;
204
+ }
205
+ if (normalizedTool === 'skill') {
206
+ const name = getString(inputData, ['name', 'skill'], 'unknown');
207
+ return `Using tool: skill - \`${name}\``;
208
+ }
209
+ if (normalizedTool === 'question') {
210
+ const text = getString(inputData, ['text', 'question', 'message'], '');
211
+ return text ? `Asking: ${truncateText(text, 100)}` : 'Using tool: question';
212
+ }
213
+ if (normalizedTool === 'lsp') {
214
+ const command = getString(inputData, ['command', 'method'], 'unknown');
215
+ const file = getString(inputData, ['file', 'path'], '');
216
+ return file ? `Using tool: lsp - ${command} on \`${file}\`` : `Using tool: lsp - ${command}`;
217
+ }
218
+ if (normalizedTool === 'todowrite') {
219
+ const todos = getArray(inputData, ['todos']);
220
+ if (!todos.length) {
221
+ return 'Using tool: TodoWrite - clearing todo list';
222
+ }
223
+ const statusSymbol = {
224
+ pending: '○',
225
+ in_progress: '◐',
226
+ completed: '●',
227
+ };
228
+ const lines = ['Using tool: TodoWrite - Todo List', ''];
229
+ for (const todo of todos) {
230
+ const status = typeof todo.status === 'string' ? todo.status : 'pending';
231
+ const content = typeof todo.content === 'string' ? todo.content : '';
232
+ const symbol = statusSymbol[status] ?? '•';
233
+ lines.push(`${symbol} ${truncateText(content, 100)}`);
234
+ }
235
+ return lines.join('\n');
236
+ }
237
+ if (normalizedTool === 'todoread') {
238
+ return 'Using tool: todoread';
239
+ }
240
+ if (normalizedTool === 'task') {
241
+ const description = getString(inputData, ['description'], 'unknown task');
242
+ const subagentType = getString(inputData, ['subagent_type', 'subagentType', 'agent'], 'unknown');
243
+ return `Using tool: Task - ${truncateText(description, 50)} (agent: ${subagentType})`;
244
+ }
245
+ if (normalizedTool === 'webfetch') {
246
+ const url = getString(inputData, ['url'], 'unknown');
247
+ return `Using tool: WebFetch - \`${truncateText(url, 80)}\``;
248
+ }
249
+ if (normalizedTool === 'websearch') {
250
+ const query = getString(inputData, ['query'], 'unknown');
251
+ return `Using tool: WebSearch - ${truncateText(query, 80)}`;
252
+ }
253
+ if (toolName === 'ListMcpResourcesTool') {
254
+ return 'Using tool: List MCP Resources';
255
+ }
256
+ const defaultKeys = ['file', 'path', 'query', 'content', 'message', 'description', 'name'];
257
+ for (const key of defaultKeys) {
258
+ if (typeof inputData[key] === 'string') {
259
+ return `Using tool: ${toolName} - ${truncateText(String(inputData[key]), 50)}`;
260
+ }
261
+ }
262
+ return `Using tool: ${toolName}`;
263
+ }
264
+ // Most tool outputs don't need truncation - show them in full.
265
+ // Only truncate for tools that tend to produce very large/noisy output.
266
+ const TRUNCATE_OUTPUT_TOOLS = new Set(['']); // set to empty for not to filter message now.
267
+ const TRUNCATE_LIMIT = 500;
268
+ export function formatToolResult(output, toolName) {
269
+ try {
270
+ const parsed = JSON.parse(output);
271
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
272
+ const keys = Object.keys(parsed).slice(0, 3);
273
+ const summary = `JSON object with keys: ${keys.join(', ')}`;
274
+ return keys.length < Object.keys(parsed).length ? `${summary} and ${Object.keys(parsed).length - keys.length} more` : summary;
275
+ }
276
+ }
277
+ catch {
278
+ // not JSON
279
+ }
280
+ // Default: don't truncate tool output unless it's in the truncate list
281
+ if (toolName && TRUNCATE_OUTPUT_TOOLS.has(toolName.toLowerCase())) {
282
+ return truncateText(output, TRUNCATE_LIMIT);
283
+ }
284
+ return output;
285
+ }
286
+ // Tools whose Result line is noise: either raw file/list content (read-side)
287
+ // or a boilerplate success confirmation like "Edit applied successfully."
288
+ // (write-side). Only the usage line is shown for these.
289
+ const SUPPRESS_OUTPUT_TOOLS = new Set([
290
+ 'read', 'notebookread', 'list', 'ls', 'glob', 'grep', 'todoread', 'lsp',
291
+ 'write', 'edit', 'multiedit', 'patch', 'notebookedit', 'todowrite',
292
+ ]);
293
+ export function shouldSuppressToolOutput(toolName) {
294
+ return SUPPRESS_OUTPUT_TOOLS.has(toolName.toLowerCase());
295
+ }
296
+ export function formatToolPart(toolPart) {
297
+ const base = formatToolUsage(toolPart.tool, toolPart.state.input ?? {});
298
+ if (toolPart.state.status === 'completed') {
299
+ if (shouldSuppressToolOutput(toolPart.tool)) {
300
+ return base;
301
+ }
302
+ const result = toolPart.state.output ? formatToolResult(toolPart.state.output, toolPart.tool) : '[empty]';
303
+ return `${base}\nResult: ${result}`;
304
+ }
305
+ if (toolPart.state.status === 'error') {
306
+ const error = toolPart.state.error ? truncateText(toolPart.state.error, 200) : 'Unknown error';
307
+ return `${base}\nResult: Error - ${error}`;
308
+ }
309
+ return base;
310
+ }
311
+ export function formatReasoningPart(part) {
312
+ if (!part.text) {
313
+ return '';
314
+ }
315
+ return `[Thinking: ${truncateText(part.text, 200)}]`;
316
+ }
317
+ export function formatFilePart(part) {
318
+ if (part.mime?.startsWith('image/') && part.url) {
319
+ const altText = part.filename ?? 'image';
320
+ return `![${altText}](${part.url})`;
321
+ }
322
+ const source = part.source?.text?.value ?? '';
323
+ const path = part.source?.path ?? part.filename ?? part.url ?? 'file';
324
+ if (source) {
325
+ const lang = detectLanguage(path);
326
+ return [`File: \`${path}\``, `\`\`\`${lang}`, source, '```'].join('\n');
327
+ }
328
+ return `File: \`${path}\``;
329
+ }
330
+ export function formatPatchPart(part) {
331
+ if (part.files?.length) {
332
+ return `Patch updated: ${part.files.join(', ')}`;
333
+ }
334
+ return 'Patch updated';
335
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * opencode-vicoa
3
+ *
4
+ * OpenCode plugin that integrates with Vicoa web & mobile
5
+ * - Real-time session monitoring
6
+ * - Bidirectional messaging (UI ↔ agent)
7
+ * - Permission request forwarding
8
+ * - Tool execution tracking
9
+ * - Session state management
10
+ *
11
+ * This plugin runs INSIDE OpenCode and directly calls Vicoa REST APIs,
12
+ * eliminating the need for a separate Python wrapper process.
13
+ */
14
+ import type { Plugin } from '@opencode-ai/plugin';
15
+ export declare const VicoaPlugin: Plugin;
16
+ export default VicoaPlugin;