@vicoa/opencode 0.1.0 → 0.1.1
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 +1 -1
- package/package.json +2 -1
- package/dist/commands.d.ts +0 -24
- package/dist/commands.js +0 -228
- package/dist/credentials.d.ts +0 -27
- package/dist/credentials.js +0 -56
- package/dist/format-utils.d.ts +0 -10
- package/dist/format-utils.js +0 -335
- package/dist/message-poller.d.ts +0 -16
- package/dist/message-poller.js +0 -45
- package/dist/plugin/file-sync.d.ts +0 -5
- package/dist/plugin/file-sync.js +0 -187
- package/dist/vicoa-client.d.ts +0 -67
- package/dist/vicoa-client.js +0 -259
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vicoa/opencode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "OpenCode plugin for using OpenCode anywhere, on any device with Vicoa",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
],
|
|
35
35
|
"author": "Vicoa",
|
|
36
36
|
"license": "Apache-2.0",
|
|
37
|
+
"homepage": "https://vicoa.ai",
|
|
37
38
|
"repository": {
|
|
38
39
|
"type": "git",
|
|
39
40
|
"url": "https://github.com/vicoa-ai/opencode-vicoa.git"
|
package/dist/commands.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { VicoaClient } from './vicoa-client.js';
|
|
2
|
-
export declare const OPENCODE_SLASH_AGENT_TYPE = "opencode";
|
|
3
|
-
export declare const OPENCODE_SLASH_COMMAND_ACTIONS: Record<string, string>;
|
|
4
|
-
export declare const OPENCODE_EXECUTE_COMMAND_KEYS: Record<string, string>;
|
|
5
|
-
/**
|
|
6
|
-
* Execute a TUI command via the OpenCode client.
|
|
7
|
-
*/
|
|
8
|
-
export declare function executeTuiCommand(client: any, command: string): Promise<void>;
|
|
9
|
-
export type OpencodeCommandMap = Record<string, {
|
|
10
|
-
description: string;
|
|
11
|
-
}>;
|
|
12
|
-
export type ParsedSlashCommand = {
|
|
13
|
-
name: string;
|
|
14
|
-
rawName: string;
|
|
15
|
-
arguments: string;
|
|
16
|
-
};
|
|
17
|
-
export declare function parseSlashCommand(input: string): ParsedSlashCommand | null;
|
|
18
|
-
export declare function scanOpencodeCommands(projectDir: string | undefined, homeDir: string): Promise<OpencodeCommandMap>;
|
|
19
|
-
/**
|
|
20
|
-
* Handle slash command execution. Returns true if the command was executed
|
|
21
|
-
* directly (built-in with no arguments), false if it should be submitted as
|
|
22
|
-
* a prompt (built-in with args, custom command, or unknown command).
|
|
23
|
-
*/
|
|
24
|
-
export declare function handleSlashCommand(userMessage: string, client: any, currentSessionId?: string, vicoaClient?: VicoaClient): Promise<boolean>;
|
package/dist/commands.js
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import * as fs from 'fs/promises';
|
|
3
|
-
import { log } from './plugin/utils.js';
|
|
4
|
-
export const OPENCODE_SLASH_AGENT_TYPE = 'opencode';
|
|
5
|
-
export const OPENCODE_SLASH_COMMAND_ACTIONS = {
|
|
6
|
-
sessions: 'session.list',
|
|
7
|
-
resume: 'session.list',
|
|
8
|
-
continue: 'session.list',
|
|
9
|
-
new: 'session.new',
|
|
10
|
-
clear: 'session.new',
|
|
11
|
-
models: 'model.list',
|
|
12
|
-
agents: 'agent.list',
|
|
13
|
-
mcps: 'mcp.list',
|
|
14
|
-
connect: 'provider.connect',
|
|
15
|
-
status: 'opencode.status',
|
|
16
|
-
themes: 'theme.switch',
|
|
17
|
-
help: 'help.show',
|
|
18
|
-
exit: 'app.exit',
|
|
19
|
-
quit: 'app.exit',
|
|
20
|
-
q: 'app.exit',
|
|
21
|
-
editor: 'prompt.editor',
|
|
22
|
-
share: 'session.share',
|
|
23
|
-
rename: 'session.rename',
|
|
24
|
-
timeline: 'session.timeline',
|
|
25
|
-
fork: 'session.fork',
|
|
26
|
-
compact: 'session.compact',
|
|
27
|
-
summarize: 'session.compact',
|
|
28
|
-
unshare: 'session.unshare',
|
|
29
|
-
undo: 'session.undo',
|
|
30
|
-
redo: 'session.redo',
|
|
31
|
-
timestamps: 'session.toggle.timestamps',
|
|
32
|
-
'toggle-timestamps': 'session.toggle.timestamps',
|
|
33
|
-
thinking: 'session.toggle.thinking',
|
|
34
|
-
'toggle-thinking': 'session.toggle.thinking',
|
|
35
|
-
copy: 'session.copy',
|
|
36
|
-
export: 'session.export',
|
|
37
|
-
};
|
|
38
|
-
export const OPENCODE_EXECUTE_COMMAND_KEYS = {
|
|
39
|
-
'session.new': 'session_new',
|
|
40
|
-
'session.share': 'session_share',
|
|
41
|
-
'session.interrupt': 'session_interrupt',
|
|
42
|
-
'session.compact': 'session_compact',
|
|
43
|
-
'session.page.up': 'messages_page_up',
|
|
44
|
-
'session.page.down': 'messages_page_down',
|
|
45
|
-
'session.line.up': 'messages_line_up',
|
|
46
|
-
'session.line.down': 'messages_line_down',
|
|
47
|
-
'session.half.page.up': 'messages_half_page_up',
|
|
48
|
-
'session.half.page.down': 'messages_half_page_down',
|
|
49
|
-
'session.first': 'messages_first',
|
|
50
|
-
'session.last': 'messages_last',
|
|
51
|
-
'agent.cycle': 'agent_cycle',
|
|
52
|
-
};
|
|
53
|
-
/**
|
|
54
|
-
* Execute a TUI command via the OpenCode client.
|
|
55
|
-
*/
|
|
56
|
-
export async function executeTuiCommand(client, command) {
|
|
57
|
-
const executeKey = OPENCODE_EXECUTE_COMMAND_KEYS[command];
|
|
58
|
-
if (executeKey) {
|
|
59
|
-
await client.tui.executeCommand({ body: { command: executeKey } });
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
await client.tui.publish({
|
|
63
|
-
body: {
|
|
64
|
-
type: 'tui.command.execute',
|
|
65
|
-
properties: { command },
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
export function parseSlashCommand(input) {
|
|
70
|
-
const trimmed = input.trim();
|
|
71
|
-
if (!trimmed.startsWith('/')) {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
const body = trimmed.slice(1).trim();
|
|
75
|
-
if (!body) {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
const [rawName, ...rest] = body.split(/\s+/);
|
|
79
|
-
if (!rawName) {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
return {
|
|
83
|
-
rawName,
|
|
84
|
-
name: rawName.toLowerCase(),
|
|
85
|
-
arguments: rest.join(' ').trim(),
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
async function pathExists(target) {
|
|
89
|
-
try {
|
|
90
|
-
await fs.stat(target);
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
async function walkMarkdownFiles(root) {
|
|
98
|
-
const results = [];
|
|
99
|
-
if (!(await pathExists(root))) {
|
|
100
|
-
return results;
|
|
101
|
-
}
|
|
102
|
-
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
103
|
-
for (const entry of entries) {
|
|
104
|
-
const fullPath = path.join(root, entry.name);
|
|
105
|
-
if (entry.isDirectory()) {
|
|
106
|
-
results.push(...(await walkMarkdownFiles(fullPath)));
|
|
107
|
-
}
|
|
108
|
-
else if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) {
|
|
109
|
-
results.push(fullPath);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return results;
|
|
113
|
-
}
|
|
114
|
-
function getOpencodeCommandRoots(projectDir, homeDir) {
|
|
115
|
-
const roots = new Set();
|
|
116
|
-
if (process.env.OPENCODE_CONFIG_DIR) {
|
|
117
|
-
roots.add(process.env.OPENCODE_CONFIG_DIR);
|
|
118
|
-
}
|
|
119
|
-
if (process.env.XDG_CONFIG_HOME) {
|
|
120
|
-
roots.add(path.join(process.env.XDG_CONFIG_HOME, 'opencode'));
|
|
121
|
-
}
|
|
122
|
-
roots.add(path.join(homeDir, '.config', 'opencode'));
|
|
123
|
-
if (process.platform === 'win32') {
|
|
124
|
-
const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
|
|
125
|
-
roots.add(path.join(appData, 'opencode'));
|
|
126
|
-
}
|
|
127
|
-
// ~/.opencode fallback (all platforms).
|
|
128
|
-
roots.add(path.join(homeDir, '.opencode'));
|
|
129
|
-
// Per-project override.
|
|
130
|
-
if (projectDir) {
|
|
131
|
-
roots.add(path.join(projectDir, '.opencode'));
|
|
132
|
-
}
|
|
133
|
-
return Array.from(roots);
|
|
134
|
-
}
|
|
135
|
-
function parseCommandDescription(content, fallbackName) {
|
|
136
|
-
const lines = content.split('\n');
|
|
137
|
-
let description = '';
|
|
138
|
-
if (lines.length > 0 && lines[0].trim() === '---') {
|
|
139
|
-
for (let i = 1; i < lines.length; i += 1) {
|
|
140
|
-
const line = lines[i].trim();
|
|
141
|
-
if (line === '---') {
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
|
-
const [key, value] = line.split(':', 2).map((item) => item?.trim());
|
|
145
|
-
if (key?.toLowerCase() === 'description' && value) {
|
|
146
|
-
description = value.replace(/^['"]|['"]$/g, '').trim();
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
if (!description) {
|
|
152
|
-
for (const line of lines) {
|
|
153
|
-
const stripped = line.trim();
|
|
154
|
-
if (!stripped) {
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
description = stripped.startsWith('#') ? stripped.replace(/^#+/, '').trim() : stripped;
|
|
158
|
-
break;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return description || `Custom command: ${fallbackName}`;
|
|
162
|
-
}
|
|
163
|
-
export async function scanOpencodeCommands(projectDir, homeDir) {
|
|
164
|
-
const commands = {};
|
|
165
|
-
const roots = getOpencodeCommandRoots(projectDir, homeDir);
|
|
166
|
-
for (const root of roots) {
|
|
167
|
-
const commandRoot = path.join(root, 'commands');
|
|
168
|
-
const files = await walkMarkdownFiles(commandRoot);
|
|
169
|
-
for (const filePath of files) {
|
|
170
|
-
const relativePath = path.relative(commandRoot, filePath);
|
|
171
|
-
const normalized = relativePath.split(path.sep).join('/');
|
|
172
|
-
const commandName = normalized.replace(/\.md$/i, '');
|
|
173
|
-
if (!commandName) {
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
try {
|
|
177
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
178
|
-
const description = parseCommandDescription(content, commandName);
|
|
179
|
-
commands[commandName] = { description };
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return commands;
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Handle slash command execution. Returns true if the command was executed
|
|
190
|
-
* directly (built-in with no arguments), false if it should be submitted as
|
|
191
|
-
* a prompt (built-in with args, custom command, or unknown command).
|
|
192
|
-
*/
|
|
193
|
-
export async function handleSlashCommand(userMessage, client, currentSessionId, vicoaClient) {
|
|
194
|
-
const parsed = parseSlashCommand(userMessage);
|
|
195
|
-
if (!parsed) {
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
const action = OPENCODE_SLASH_COMMAND_ACTIONS[parsed.name];
|
|
199
|
-
// Built-in command with no arguments — use the direct TUI action shortcut.
|
|
200
|
-
if (action && !parsed.arguments) {
|
|
201
|
-
// Special handling for /share command: call the API directly to get the URL
|
|
202
|
-
if (parsed.name === 'share' && currentSessionId && vicoaClient) {
|
|
203
|
-
try {
|
|
204
|
-
const { data: session } = await client.session.share({
|
|
205
|
-
path: { id: currentSessionId },
|
|
206
|
-
});
|
|
207
|
-
if (session?.share?.url) {
|
|
208
|
-
await vicoaClient.sendMessage(`Share url: ${session.share.url}`);
|
|
209
|
-
log(client, 'info', `[Vicoa] Shared session and sent URL to UI: ${session.share.url}`);
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
log(client, 'warn', '[Vicoa] Session shared but no URL in response');
|
|
213
|
-
}
|
|
214
|
-
return true;
|
|
215
|
-
}
|
|
216
|
-
catch (error) {
|
|
217
|
-
log(client, 'warn', `[Vicoa] Failed to share session: ${error}`);
|
|
218
|
-
// Fall back to TUI command if API call fails
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
await executeTuiCommand(client, action);
|
|
222
|
-
log(client, 'info', `[Vicoa] Executed slash command: /${parsed.rawName}`);
|
|
223
|
-
return true;
|
|
224
|
-
}
|
|
225
|
-
// Built-in with arguments, or a custom command — fall through so the raw
|
|
226
|
-
// slash command text is submitted as a prompt (OpenCode parses args natively).
|
|
227
|
-
return false;
|
|
228
|
-
}
|
package/dist/credentials.d.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Credentials loader for Vicoa
|
|
3
|
-
*
|
|
4
|
-
* Reads API key from ~/.vicoa/credentials.json (same as Vicoa CLI)
|
|
5
|
-
*/
|
|
6
|
-
export interface Credentials {
|
|
7
|
-
write_key?: string;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Get path to Vicoa credentials file
|
|
11
|
-
*/
|
|
12
|
-
export declare function getCredentialsPath(): string;
|
|
13
|
-
/**
|
|
14
|
-
* Load Vicoa API key from credentials file
|
|
15
|
-
*
|
|
16
|
-
* Returns the API key from ~/.vicoa/credentials.json if it exists,
|
|
17
|
-
* otherwise returns null.
|
|
18
|
-
*/
|
|
19
|
-
export declare function loadApiKey(): string | null;
|
|
20
|
-
/**
|
|
21
|
-
* Get Vicoa API key from environment or credentials file
|
|
22
|
-
*
|
|
23
|
-
* Priority:
|
|
24
|
-
* 1. VICOA_API_KEY environment variable
|
|
25
|
-
* 2. ~/.vicoa/credentials.json (write_key)
|
|
26
|
-
*/
|
|
27
|
-
export declare function getApiKey(): string | null;
|
package/dist/credentials.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Credentials loader for Vicoa
|
|
3
|
-
*
|
|
4
|
-
* Reads API key from ~/.vicoa/credentials.json (same as Vicoa CLI)
|
|
5
|
-
*/
|
|
6
|
-
import * as fs from 'fs';
|
|
7
|
-
import * as os from 'os';
|
|
8
|
-
import * as path from 'path';
|
|
9
|
-
/**
|
|
10
|
-
* Get path to Vicoa credentials file
|
|
11
|
-
*/
|
|
12
|
-
export function getCredentialsPath() {
|
|
13
|
-
return path.join(os.homedir(), '.vicoa', 'credentials.json');
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Load Vicoa API key from credentials file
|
|
17
|
-
*
|
|
18
|
-
* Returns the API key from ~/.vicoa/credentials.json if it exists,
|
|
19
|
-
* otherwise returns null.
|
|
20
|
-
*/
|
|
21
|
-
export function loadApiKey() {
|
|
22
|
-
const credentialsPath = getCredentialsPath();
|
|
23
|
-
// Check if file exists
|
|
24
|
-
if (!fs.existsSync(credentialsPath)) {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
try {
|
|
28
|
-
const data = fs.readFileSync(credentialsPath, 'utf-8');
|
|
29
|
-
const credentials = JSON.parse(data);
|
|
30
|
-
const apiKey = credentials.write_key;
|
|
31
|
-
if (apiKey && typeof apiKey === 'string' && apiKey.trim().length > 0) {
|
|
32
|
-
return apiKey.trim();
|
|
33
|
-
}
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
console.error(`[Vicoa] Error reading credentials file: ${error}`);
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Get Vicoa API key from environment or credentials file
|
|
43
|
-
*
|
|
44
|
-
* Priority:
|
|
45
|
-
* 1. VICOA_API_KEY environment variable
|
|
46
|
-
* 2. ~/.vicoa/credentials.json (write_key)
|
|
47
|
-
*/
|
|
48
|
-
export function getApiKey() {
|
|
49
|
-
// Check environment variable first
|
|
50
|
-
const envKey = process.env.VICOA_API_KEY;
|
|
51
|
-
if (envKey && envKey.trim().length > 0) {
|
|
52
|
-
return envKey.trim();
|
|
53
|
-
}
|
|
54
|
-
// Fall back to credentials file
|
|
55
|
-
return loadApiKey();
|
|
56
|
-
}
|
package/dist/format-utils.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { FilePart, PatchPart, ReasoningPart, ToolPart } from '@opencode-ai/sdk';
|
|
2
|
-
type ToolInput = Record<string, unknown>;
|
|
3
|
-
export declare function formatToolUsage(toolName: string, inputData: ToolInput): string;
|
|
4
|
-
export declare function formatToolResult(output: string, toolName?: string): string;
|
|
5
|
-
export declare function shouldSuppressToolOutput(toolName: string): boolean;
|
|
6
|
-
export declare function formatToolPart(toolPart: ToolPart): string;
|
|
7
|
-
export declare function formatReasoningPart(part: ReasoningPart): string;
|
|
8
|
-
export declare function formatFilePart(part: FilePart): string;
|
|
9
|
-
export declare function formatPatchPart(part: PatchPart): string;
|
|
10
|
-
export {};
|
package/dist/format-utils.js
DELETED
|
@@ -1,335 +0,0 @@
|
|
|
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 ``;
|
|
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
|
-
}
|
package/dist/message-poller.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Polls Vicoa backend for user messages and sends them to OpenCode
|
|
3
|
-
*
|
|
4
|
-
* This mimics the Claude wrapper's message queue and polling functionality.
|
|
5
|
-
*/
|
|
6
|
-
import type { VicoaClient } from './vicoa-client.js';
|
|
7
|
-
export declare class MessagePoller {
|
|
8
|
-
private client;
|
|
9
|
-
private interval;
|
|
10
|
-
private pollIntervalMs;
|
|
11
|
-
private onMessage;
|
|
12
|
-
private log;
|
|
13
|
-
constructor(client: VicoaClient, onMessage: (content: string) => Promise<void>, logFunc?: (level: string, msg: string) => void, pollIntervalMs?: number);
|
|
14
|
-
start(): void;
|
|
15
|
-
stop(): void;
|
|
16
|
-
}
|
package/dist/message-poller.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Polls Vicoa backend for user messages and sends them to OpenCode
|
|
3
|
-
*
|
|
4
|
-
* This mimics the Claude wrapper's message queue and polling functionality.
|
|
5
|
-
*/
|
|
6
|
-
export class MessagePoller {
|
|
7
|
-
client;
|
|
8
|
-
interval = null;
|
|
9
|
-
pollIntervalMs;
|
|
10
|
-
onMessage;
|
|
11
|
-
log;
|
|
12
|
-
constructor(client, onMessage, logFunc, pollIntervalMs = 1000) {
|
|
13
|
-
this.client = client;
|
|
14
|
-
this.onMessage = onMessage;
|
|
15
|
-
this.pollIntervalMs = pollIntervalMs;
|
|
16
|
-
this.log = logFunc || ((level, msg) => console.log(`[${level}] ${msg}`));
|
|
17
|
-
}
|
|
18
|
-
start() {
|
|
19
|
-
if (this.interval) {
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
this.log('info', 'Starting message poller');
|
|
23
|
-
this.interval = setInterval(async () => {
|
|
24
|
-
try {
|
|
25
|
-
const messages = await this.client.getPendingMessages();
|
|
26
|
-
for (const msg of messages) {
|
|
27
|
-
if (msg.sender_type === 'USER' && msg.content) {
|
|
28
|
-
this.log('debug', `Received user message: ${msg.content.substring(0, 100)}...`);
|
|
29
|
-
await this.onMessage(msg.content);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
this.log('warn', `Error polling messages: ${error}`);
|
|
35
|
-
}
|
|
36
|
-
}, this.pollIntervalMs);
|
|
37
|
-
}
|
|
38
|
-
stop() {
|
|
39
|
-
if (this.interval) {
|
|
40
|
-
clearInterval(this.interval);
|
|
41
|
-
this.interval = null;
|
|
42
|
-
this.log('info', 'Stopped message poller');
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
package/dist/plugin/file-sync.js
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs/promises';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import { formatProjectPath } from './path-utils.js';
|
|
4
|
-
// Common patterns to always exclude (matching Python version)
|
|
5
|
-
const DEFAULT_EXCLUDE_PATTERNS = [
|
|
6
|
-
'.git/',
|
|
7
|
-
'.pytest_cache/',
|
|
8
|
-
'.dart_tool/',
|
|
9
|
-
'.ruff_cache/',
|
|
10
|
-
'.mypy_cache/',
|
|
11
|
-
'__pycache__/',
|
|
12
|
-
'node_modules/',
|
|
13
|
-
'.venv/',
|
|
14
|
-
'venv/',
|
|
15
|
-
'.tox/',
|
|
16
|
-
'.eggs/',
|
|
17
|
-
'*.egg-info/',
|
|
18
|
-
'dist/',
|
|
19
|
-
'build/',
|
|
20
|
-
'target/', // Rust build output
|
|
21
|
-
'.next/', // Next.js
|
|
22
|
-
'.nuxt/', // Nuxt
|
|
23
|
-
'coverage/',
|
|
24
|
-
'.coverage',
|
|
25
|
-
'.nyc_output/',
|
|
26
|
-
'*.pyc',
|
|
27
|
-
'*.pyo',
|
|
28
|
-
'*.pyd',
|
|
29
|
-
];
|
|
30
|
-
// Extract directory names from exclude patterns
|
|
31
|
-
function getExcludedDirNames() {
|
|
32
|
-
const excluded = new Set();
|
|
33
|
-
for (const pattern of DEFAULT_EXCLUDE_PATTERNS) {
|
|
34
|
-
// Skip file patterns (contain wildcards)
|
|
35
|
-
if (pattern.includes('*')) {
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
// Strip trailing slashes and add to set
|
|
39
|
-
const dirName = pattern.replace(/\/$/, '');
|
|
40
|
-
if (dirName) {
|
|
41
|
-
excluded.add(dirName);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return excluded;
|
|
45
|
-
}
|
|
46
|
-
// Load .gitignore patterns
|
|
47
|
-
async function loadGitignorePatterns(projectPath) {
|
|
48
|
-
const gitignorePath = path.join(projectPath, '.gitignore');
|
|
49
|
-
try {
|
|
50
|
-
const content = await fs.readFile(gitignorePath, 'utf-8');
|
|
51
|
-
return content
|
|
52
|
-
.split('\n')
|
|
53
|
-
.map(line => line.trim())
|
|
54
|
-
.filter(line => line && !line.startsWith('#'));
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return [];
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
// Simple gitignore pattern matcher (basic implementation)
|
|
61
|
-
function matchesGitignorePattern(filePath, patterns) {
|
|
62
|
-
for (const pattern of patterns) {
|
|
63
|
-
const normalizedPattern = pattern.trim();
|
|
64
|
-
if (!normalizedPattern)
|
|
65
|
-
continue;
|
|
66
|
-
// Convert to regex for simple matching
|
|
67
|
-
const regexPattern = normalizedPattern
|
|
68
|
-
.replace(/\./g, '\\.')
|
|
69
|
-
.replace(/\*/g, '.*')
|
|
70
|
-
.replace(/\?/g, '.');
|
|
71
|
-
const regex = new RegExp(`^${regexPattern}`);
|
|
72
|
-
// Check both relative path and basename
|
|
73
|
-
if (regex.test(filePath) || regex.test(path.basename(filePath))) {
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
// Scan project files recursively
|
|
80
|
-
async function scanProjectFiles(projectPath, vicoaClient, maxFiles = 100000) {
|
|
81
|
-
const base = path.resolve(projectPath);
|
|
82
|
-
try {
|
|
83
|
-
const stats = await fs.stat(base);
|
|
84
|
-
if (!stats.isDirectory()) {
|
|
85
|
-
return [];
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
return [];
|
|
90
|
-
}
|
|
91
|
-
const excludePatterns = [...DEFAULT_EXCLUDE_PATTERNS];
|
|
92
|
-
const gitignorePatterns = await loadGitignorePatterns(base);
|
|
93
|
-
excludePatterns.push(...gitignorePatterns);
|
|
94
|
-
const skipDirs = getExcludedDirNames();
|
|
95
|
-
const files = [];
|
|
96
|
-
const folders = new Set();
|
|
97
|
-
async function walkDirectory(dirPath, relativePath = '') {
|
|
98
|
-
try {
|
|
99
|
-
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
100
|
-
for (const entry of entries) {
|
|
101
|
-
// Skip excluded directories early
|
|
102
|
-
if (entry.isDirectory() && skipDirs.has(entry.name)) {
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
const entryRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
|
|
106
|
-
const entryFullPath = path.join(dirPath, entry.name);
|
|
107
|
-
if (entry.isDirectory()) {
|
|
108
|
-
await walkDirectory(entryFullPath, entryRelativePath);
|
|
109
|
-
// Add folder to results (with trailing slash)
|
|
110
|
-
const folderPath = entryRelativePath.replace(/\\/g, '/') + '/';
|
|
111
|
-
folders.add(folderPath);
|
|
112
|
-
}
|
|
113
|
-
else if (entry.isFile()) {
|
|
114
|
-
// Check if file should be excluded
|
|
115
|
-
if (matchesGitignorePattern(entryRelativePath, excludePatterns)) {
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
files.push(entryRelativePath.replace(/\\/g, '/'));
|
|
119
|
-
// Extract parent folders
|
|
120
|
-
const parts = entryRelativePath.split(path.sep);
|
|
121
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
122
|
-
const folderPath = parts.slice(0, i + 1).join('/') + '/';
|
|
123
|
-
folders.add(folderPath);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
// Stop at max_files limit
|
|
127
|
-
if (files.length >= maxFiles) {
|
|
128
|
-
vicoaClient.log('warn', `Large project detected: ${maxFiles}+ files found`);
|
|
129
|
-
vicoaClient.log('warn', `Only syncing first ${maxFiles} files for performance`);
|
|
130
|
-
vicoaClient.log('warn', 'File mentions may be incomplete');
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
vicoaClient.log('warn', `Error scanning directory ${dirPath}: ${error}`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
await walkDirectory(base);
|
|
140
|
-
// Combine folders and files
|
|
141
|
-
return Array.from(folders).concat(files);
|
|
142
|
-
}
|
|
143
|
-
// Sync project files to Vicoa backend
|
|
144
|
-
export async function syncProjectFiles(vicoaClient, projectPath) {
|
|
145
|
-
try {
|
|
146
|
-
// Use absolute path for file scanning (needed for fs operations)
|
|
147
|
-
const absolutePath = path.resolve(projectPath);
|
|
148
|
-
vicoaClient.log('info', 'Preparing fuzzy file search with @ ...');
|
|
149
|
-
const files = await scanProjectFiles(absolutePath, vicoaClient);
|
|
150
|
-
if (files.length === 0) {
|
|
151
|
-
return; // No files to sync
|
|
152
|
-
}
|
|
153
|
-
// Send tilde path to backend (consistent with Claude wrapper)
|
|
154
|
-
const formattedPath = formatProjectPath(absolutePath);
|
|
155
|
-
await vicoaClient.syncFiles(formattedPath, files);
|
|
156
|
-
vicoaClient.log('info', `Synced ${files.length} files for fuzzy search`);
|
|
157
|
-
}
|
|
158
|
-
catch (error) {
|
|
159
|
-
vicoaClient.log('warn', `Failed to sync project files: ${error}`);
|
|
160
|
-
// Silently fail - don't block plugin startup
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
// Backup vicoa client api call here
|
|
164
|
-
/**
|
|
165
|
-
* Sync project files to Vicoa backend for @ mentions
|
|
166
|
-
*/
|
|
167
|
-
// async syncFiles(projectPath: string, files: string[]): Promise<void> {
|
|
168
|
-
// try {
|
|
169
|
-
// const response = await fetch(`${this.config.baseUrl}/api/v1/files/sync`, {
|
|
170
|
-
// method: 'POST',
|
|
171
|
-
// headers: {
|
|
172
|
-
// 'Authorization': `Bearer ${this.config.apiKey}`,
|
|
173
|
-
// 'Content-Type': 'application/json',
|
|
174
|
-
// },
|
|
175
|
-
// body: JSON.stringify({
|
|
176
|
-
// project_path: projectPath,
|
|
177
|
-
// files,
|
|
178
|
-
// }),
|
|
179
|
-
// });
|
|
180
|
-
// if (!response.ok) {
|
|
181
|
-
// const error = await response.text();
|
|
182
|
-
// this.log('warn', `Failed to sync project files: ${response.statusText} - ${error}`);
|
|
183
|
-
// }
|
|
184
|
-
// } catch (error) {
|
|
185
|
-
// this.log('warn', `Error syncing project files: ${error}`);
|
|
186
|
-
// }
|
|
187
|
-
// }
|
package/dist/vicoa-client.d.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vicoa API client for OpenCode plugin
|
|
3
|
-
*
|
|
4
|
-
* This client uses Vicoa's existing REST APIs to communicate with the dashboard.
|
|
5
|
-
* It mimics the Python VicoaClient functionality but runs in TypeScript/Node.js.
|
|
6
|
-
*/
|
|
7
|
-
export interface VicoaClientConfig {
|
|
8
|
-
apiKey: string;
|
|
9
|
-
baseUrl: string;
|
|
10
|
-
agentType: string;
|
|
11
|
-
agentInstanceId: string;
|
|
12
|
-
logFunc?: (level: string, msg: string) => void;
|
|
13
|
-
}
|
|
14
|
-
export interface VicoaMessage {
|
|
15
|
-
id: string;
|
|
16
|
-
content: string;
|
|
17
|
-
sender_type: 'USER' | 'AGENT';
|
|
18
|
-
requires_user_input: boolean;
|
|
19
|
-
created_at: string;
|
|
20
|
-
}
|
|
21
|
-
export declare class VicoaClient {
|
|
22
|
-
private config;
|
|
23
|
-
lastMessageId: string | null;
|
|
24
|
-
log: (level: string, msg: string) => void;
|
|
25
|
-
constructor(config: VicoaClientConfig);
|
|
26
|
-
/**
|
|
27
|
-
* Register agent instance with Vicoa backend
|
|
28
|
-
*/
|
|
29
|
-
registerAgentInstance(project: string, homeDir: string): Promise<{
|
|
30
|
-
agent_instance_id: string;
|
|
31
|
-
}>;
|
|
32
|
-
/**
|
|
33
|
-
* Sync custom slash commands to Vicoa backend
|
|
34
|
-
*/
|
|
35
|
-
syncCommands(agentType: string, commands: Record<string, {
|
|
36
|
-
description: string;
|
|
37
|
-
}>): Promise<void>;
|
|
38
|
-
/**
|
|
39
|
-
* Send agent message to Vicoa dashboard
|
|
40
|
-
*/
|
|
41
|
-
sendMessage(content: string, requiresUserInput?: boolean): Promise<string | null>;
|
|
42
|
-
/**
|
|
43
|
-
* Send user message from terminal to Vicoa dashboard
|
|
44
|
-
* This is used when the user types a message directly in the OpenCode terminal
|
|
45
|
-
*/
|
|
46
|
-
sendUserMessage(content: string): Promise<string | null>;
|
|
47
|
-
/**
|
|
48
|
-
* Poll for pending user messages from Vicoa dashboard
|
|
49
|
-
*/
|
|
50
|
-
getPendingMessages(): Promise<VicoaMessage[]>;
|
|
51
|
-
/**
|
|
52
|
-
* Request user input (equivalent to Claude wrapper's request_user_input)
|
|
53
|
-
*/
|
|
54
|
-
requestUserInput(messageId: string): Promise<void>;
|
|
55
|
-
/**
|
|
56
|
-
* Update agent instance status
|
|
57
|
-
*/
|
|
58
|
-
updateStatus(status: 'ACTIVE' | 'AWAITING_INPUT' | 'PAUSED' | 'STALE' | 'COMPLETED' | 'FAILED' | 'KILLED' | 'DISCONNECTED' | 'DELETED'): Promise<void>;
|
|
59
|
-
/**
|
|
60
|
-
* Update agent instance title/name
|
|
61
|
-
*/
|
|
62
|
-
updateAgentInstanceName(name: string): Promise<void>;
|
|
63
|
-
/**
|
|
64
|
-
* End session
|
|
65
|
-
*/
|
|
66
|
-
endSession(): Promise<void>;
|
|
67
|
-
}
|
package/dist/vicoa-client.js
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vicoa API client for OpenCode plugin
|
|
3
|
-
*
|
|
4
|
-
* This client uses Vicoa's existing REST APIs to communicate with the dashboard.
|
|
5
|
-
* It mimics the Python VicoaClient functionality but runs in TypeScript/Node.js.
|
|
6
|
-
*/
|
|
7
|
-
export class VicoaClient {
|
|
8
|
-
config;
|
|
9
|
-
lastMessageId = null;
|
|
10
|
-
log;
|
|
11
|
-
constructor(config) {
|
|
12
|
-
this.config = config;
|
|
13
|
-
this.log = config.logFunc || ((level, msg) => console.log(`[${level}] ${msg}`));
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Register agent instance with Vicoa backend
|
|
17
|
-
*/
|
|
18
|
-
async registerAgentInstance(project, homeDir) {
|
|
19
|
-
const response = await fetch(`${this.config.baseUrl}/api/v1/agent-instances`, {
|
|
20
|
-
method: 'POST',
|
|
21
|
-
headers: {
|
|
22
|
-
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
23
|
-
'Content-Type': 'application/json',
|
|
24
|
-
},
|
|
25
|
-
body: JSON.stringify({
|
|
26
|
-
agent_type: this.config.agentType,
|
|
27
|
-
transport: 'local',
|
|
28
|
-
agent_instance_id: this.config.agentInstanceId,
|
|
29
|
-
name: this.config.agentType,
|
|
30
|
-
project,
|
|
31
|
-
home_dir: homeDir,
|
|
32
|
-
}),
|
|
33
|
-
});
|
|
34
|
-
if (!response.ok) {
|
|
35
|
-
const error = await response.text();
|
|
36
|
-
throw new Error(`Failed to register agent instance: ${response.statusText} - ${error}`);
|
|
37
|
-
}
|
|
38
|
-
return (await response.json());
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Sync custom slash commands to Vicoa backend
|
|
42
|
-
*/
|
|
43
|
-
async syncCommands(agentType, commands) {
|
|
44
|
-
try {
|
|
45
|
-
const response = await fetch(`${this.config.baseUrl}/api/v1/commands/sync`, {
|
|
46
|
-
method: 'POST',
|
|
47
|
-
headers: {
|
|
48
|
-
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
49
|
-
'Content-Type': 'application/json',
|
|
50
|
-
},
|
|
51
|
-
body: JSON.stringify({
|
|
52
|
-
agent_type: agentType,
|
|
53
|
-
commands,
|
|
54
|
-
}),
|
|
55
|
-
});
|
|
56
|
-
if (!response.ok) {
|
|
57
|
-
const error = await response.text();
|
|
58
|
-
this.log('warn', `Failed to sync slash commands: ${response.statusText} - ${error}`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
catch (error) {
|
|
62
|
-
this.log('warn', `Error syncing slash commands: ${error}`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Send agent message to Vicoa dashboard
|
|
67
|
-
*/
|
|
68
|
-
async sendMessage(content, requiresUserInput = false) {
|
|
69
|
-
try {
|
|
70
|
-
const response = await fetch(`${this.config.baseUrl}/api/v1/messages/agent`, {
|
|
71
|
-
method: 'POST',
|
|
72
|
-
headers: {
|
|
73
|
-
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
74
|
-
'Content-Type': 'application/json',
|
|
75
|
-
},
|
|
76
|
-
body: JSON.stringify({
|
|
77
|
-
content,
|
|
78
|
-
agent_type: this.config.agentType,
|
|
79
|
-
agent_instance_id: this.config.agentInstanceId,
|
|
80
|
-
requires_user_input: requiresUserInput,
|
|
81
|
-
}),
|
|
82
|
-
});
|
|
83
|
-
if (!response.ok) {
|
|
84
|
-
const error = await response.text();
|
|
85
|
-
this.log('warn', `Failed to send message: ${response.statusText} - ${error}`);
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
const result = (await response.json());
|
|
89
|
-
// Update last message ID for polling
|
|
90
|
-
if (result.message_id) {
|
|
91
|
-
this.lastMessageId = result.message_id;
|
|
92
|
-
}
|
|
93
|
-
return result.message_id;
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
this.log('warn', `Error sending message: ${error}`);
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Send user message from terminal to Vicoa dashboard
|
|
102
|
-
* This is used when the user types a message directly in the OpenCode terminal
|
|
103
|
-
*/
|
|
104
|
-
async sendUserMessage(content) {
|
|
105
|
-
try {
|
|
106
|
-
const response = await fetch(`${this.config.baseUrl}/api/v1/messages/user`, {
|
|
107
|
-
method: 'POST',
|
|
108
|
-
headers: {
|
|
109
|
-
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
110
|
-
'Content-Type': 'application/json',
|
|
111
|
-
},
|
|
112
|
-
body: JSON.stringify({
|
|
113
|
-
content,
|
|
114
|
-
agent_instance_id: this.config.agentInstanceId,
|
|
115
|
-
}),
|
|
116
|
-
});
|
|
117
|
-
if (!response.ok) {
|
|
118
|
-
const error = await response.text();
|
|
119
|
-
this.log('warn', `Failed to send user message: ${response.statusText} - ${error}`);
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
const result = (await response.json());
|
|
123
|
-
// Update last message ID
|
|
124
|
-
if (result.message_id) {
|
|
125
|
-
this.lastMessageId = result.message_id;
|
|
126
|
-
}
|
|
127
|
-
return result.message_id;
|
|
128
|
-
}
|
|
129
|
-
catch (error) {
|
|
130
|
-
this.log('warn', `Error sending user message: ${error}`);
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Poll for pending user messages from Vicoa dashboard
|
|
136
|
-
*/
|
|
137
|
-
async getPendingMessages() {
|
|
138
|
-
try {
|
|
139
|
-
const url = new URL(`${this.config.baseUrl}/api/v1/messages/pending`);
|
|
140
|
-
url.searchParams.set('agent_instance_id', this.config.agentInstanceId);
|
|
141
|
-
if (this.lastMessageId) {
|
|
142
|
-
url.searchParams.set('last_read_message_id', this.lastMessageId);
|
|
143
|
-
}
|
|
144
|
-
const response = await fetch(url.toString(), {
|
|
145
|
-
method: 'GET',
|
|
146
|
-
headers: {
|
|
147
|
-
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
148
|
-
},
|
|
149
|
-
});
|
|
150
|
-
if (!response.ok) {
|
|
151
|
-
// Polling errors are non-fatal
|
|
152
|
-
this.log('debug', `Failed to poll messages: ${response.statusText}`);
|
|
153
|
-
return [];
|
|
154
|
-
}
|
|
155
|
-
const result = (await response.json());
|
|
156
|
-
// Check if the response is stale
|
|
157
|
-
if (result.status === 'stale') {
|
|
158
|
-
this.log('debug', 'Message polling returned stale status');
|
|
159
|
-
return [];
|
|
160
|
-
}
|
|
161
|
-
const messages = result.messages || [];
|
|
162
|
-
// Update last message ID
|
|
163
|
-
if (messages.length > 0) {
|
|
164
|
-
const lastMsg = messages[messages.length - 1];
|
|
165
|
-
this.lastMessageId = lastMsg.id;
|
|
166
|
-
}
|
|
167
|
-
return messages;
|
|
168
|
-
}
|
|
169
|
-
catch (error) {
|
|
170
|
-
this.log('debug', `Error polling messages: ${error}`);
|
|
171
|
-
return [];
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Request user input (equivalent to Claude wrapper's request_user_input)
|
|
176
|
-
*/
|
|
177
|
-
async requestUserInput(messageId) {
|
|
178
|
-
try {
|
|
179
|
-
const response = await fetch(`${this.config.baseUrl}/api/v1/messages/${messageId}/request-input`, {
|
|
180
|
-
method: 'PATCH',
|
|
181
|
-
headers: {
|
|
182
|
-
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
183
|
-
},
|
|
184
|
-
});
|
|
185
|
-
if (!response.ok) {
|
|
186
|
-
this.log('warn', `Failed to request user input: ${response.statusText}`);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
catch (error) {
|
|
190
|
-
this.log('warn', `Error requesting user input: ${error}`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Update agent instance status
|
|
195
|
-
*/
|
|
196
|
-
async updateStatus(status) {
|
|
197
|
-
try {
|
|
198
|
-
const response = await fetch(`${this.config.baseUrl}/api/v1/agent-instances/${this.config.agentInstanceId}/status`, {
|
|
199
|
-
method: 'PUT',
|
|
200
|
-
headers: {
|
|
201
|
-
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
202
|
-
'Content-Type': 'application/json',
|
|
203
|
-
},
|
|
204
|
-
body: JSON.stringify({ status }),
|
|
205
|
-
});
|
|
206
|
-
if (!response.ok) {
|
|
207
|
-
this.log('warn', `Failed to update status: ${response.statusText}`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
catch (error) {
|
|
211
|
-
this.log('warn', `Error updating status: ${error}`);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Update agent instance title/name
|
|
216
|
-
*/
|
|
217
|
-
async updateAgentInstanceName(name) {
|
|
218
|
-
try {
|
|
219
|
-
const response = await fetch(`${this.config.baseUrl}/api/v1/agent-instances/${this.config.agentInstanceId}`, {
|
|
220
|
-
method: 'PATCH',
|
|
221
|
-
headers: {
|
|
222
|
-
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
223
|
-
'Content-Type': 'application/json',
|
|
224
|
-
},
|
|
225
|
-
body: JSON.stringify({ name }),
|
|
226
|
-
});
|
|
227
|
-
if (!response.ok) {
|
|
228
|
-
this.log('warn', `Failed to update agent instance name: ${response.statusText}`);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
catch (error) {
|
|
232
|
-
this.log('warn', `Error updating agent instance name: ${error}`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* End session
|
|
237
|
-
*/
|
|
238
|
-
async endSession() {
|
|
239
|
-
await this.updateStatus('COMPLETED');
|
|
240
|
-
try {
|
|
241
|
-
const response = await fetch(`${this.config.baseUrl}/api/v1/sessions/end`, {
|
|
242
|
-
method: 'POST',
|
|
243
|
-
headers: {
|
|
244
|
-
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
245
|
-
'Content-Type': 'application/json',
|
|
246
|
-
},
|
|
247
|
-
body: JSON.stringify({
|
|
248
|
-
agent_instance_id: this.config.agentInstanceId,
|
|
249
|
-
}),
|
|
250
|
-
});
|
|
251
|
-
if (!response.ok) {
|
|
252
|
-
this.log('warn', `Failed to end session: ${response.statusText}`);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
catch (error) {
|
|
256
|
-
this.log('warn', `Error ending session: ${error}`);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|