@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,228 @@
1
+ import path from 'path';
2
+ import * as fs from 'fs/promises';
3
+ import { log } from './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
+ }
@@ -0,0 +1,15 @@
1
+ import type { VicoaClient } from './vicoa-client.js';
2
+ type ControlCommandContext = {
3
+ client: any;
4
+ vicoaClient: VicoaClient;
5
+ currentSessionId?: string;
6
+ getTuiCurrentAgent: () => string | undefined;
7
+ setTuiCurrentAgent: (agent: string | undefined) => void;
8
+ setPreferredAgent: (agent: string | undefined) => void;
9
+ };
10
+ export declare function cycleTuiToAgent(client: any, targetAgent: string, currentAgent: string | undefined): Promise<boolean>;
11
+ /**
12
+ * Handle control commands from Vicoa (matching Claude wrapper pattern)
13
+ */
14
+ export declare function handleControlCommand(content: string, context: ControlCommandContext): Promise<boolean>;
15
+ export {};
@@ -0,0 +1,140 @@
1
+ import { executeTuiCommand } from './commands.js';
2
+ import { log } from './utils.js';
3
+ // Cycle the TUI's agent indicator to `targetAgent` by firing agent.cycle
4
+ // the right number of times. The TUI wraps around at the end of the list,
5
+ // so we only need (targetIndex - currentIndex + len) % len steps.
6
+ // Returns true if the cycle was attempted, false if we couldn't determine
7
+ // the current position (e.g. no user message seen yet).
8
+ export async function cycleTuiToAgent(client, targetAgent, currentAgent) {
9
+ try {
10
+ // Fetch the same filtered list the TUI uses: primary agents only, not hidden.
11
+ // hey-api returns { data, error, … } when throwOnError is false (the default).
12
+ const { data: allAgents } = await client.app.agents();
13
+ const primaryAgents = allAgents.filter((a) => a.mode !== 'subagent' && !a.hidden);
14
+ const targetIdx = primaryAgents.findIndex((a) => a.name === targetAgent);
15
+ if (targetIdx === -1)
16
+ return false; // shouldn't happen — already validated
17
+ // If we haven't seen a user message yet we don't know where the TUI is.
18
+ // Optimistic fallback: assume it's on index 0 (the default agent).
19
+ const currentName = currentAgent ?? primaryAgents[0]?.name;
20
+ const currentIdx = primaryAgents.findIndex((a) => a.name === currentName);
21
+ if (currentIdx === -1)
22
+ return false;
23
+ const steps = (targetIdx - currentIdx + primaryAgents.length) % primaryAgents.length;
24
+ for (let i = 0; i < steps; i++) {
25
+ await executeTuiCommand(client, 'agent.cycle');
26
+ }
27
+ return true;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ function extractControlPayload(content) {
34
+ const trimmed = content.trim();
35
+ try {
36
+ const parsed = JSON.parse(trimmed);
37
+ const payload = parsed.control_command ?? (parsed.type === 'control' ? parsed : null);
38
+ if (payload) {
39
+ return payload;
40
+ }
41
+ }
42
+ catch {
43
+ // Ignore and try to scan embedded JSON
44
+ }
45
+ const candidates = trimmed.match(/\{[^{}]*\}/g) ?? [];
46
+ for (const candidate of candidates) {
47
+ try {
48
+ const parsed = JSON.parse(candidate);
49
+ const payload = parsed.control_command ?? (parsed.type === 'control' ? parsed : null);
50
+ if (payload) {
51
+ return payload;
52
+ }
53
+ }
54
+ catch {
55
+ continue;
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ /**
61
+ * Handle control commands from Vicoa (matching Claude wrapper pattern)
62
+ */
63
+ export async function handleControlCommand(content, context) {
64
+ const { client, vicoaClient, currentSessionId, getTuiCurrentAgent, setPreferredAgent, setTuiCurrentAgent } = context;
65
+ // Try to parse as JSON control command
66
+ try {
67
+ const controlPayload = extractControlPayload(content);
68
+ if (controlPayload) {
69
+ const { setting, value } = controlPayload;
70
+ if (setting === 'interrupt') {
71
+ log(client, 'info', '[Vicoa] Interrupt command received');
72
+ if (!currentSessionId) {
73
+ await vicoaClient.sendMessage('Failed to interrupt');
74
+ log(client, 'warn', '[Vicoa] Interrupt failed: no active session');
75
+ return true;
76
+ }
77
+ try {
78
+ await executeTuiCommand(client, 'session.interrupt');
79
+ await executeTuiCommand(client, 'session.interrupt');
80
+ await vicoaClient.sendMessage('Interrupted');
81
+ log(client, 'info', '[Vicoa] Interrupted');
82
+ }
83
+ catch (tuiError) {
84
+ await vicoaClient.sendMessage('Failed to interrupt.');
85
+ log(client, 'error', `[Vicoa] TUI interrupt failed: ${tuiError}`);
86
+ }
87
+ return true;
88
+ }
89
+ if (setting === 'agent_type') {
90
+ const nextAgent = typeof value === 'string' ? value.toLowerCase() : '';
91
+ if (!nextAgent) {
92
+ log(client, 'warn', '[Vicoa] Agent type control command missing value');
93
+ return true;
94
+ }
95
+ let selectedAgent = nextAgent;
96
+ // Validate against the agents OpenCode actually knows about.
97
+ // Filter to primary (non-subagent, non-hidden) — same set the TUI cycles through.
98
+ try {
99
+ const { data: agents } = await client.app.agents();
100
+ const primaryAgents = agents.filter((a) => a.mode !== 'subagent' && !a.hidden);
101
+ const validNames = primaryAgents.map((a) => a.name);
102
+ const match = validNames.find((n) => n.toLowerCase() === nextAgent);
103
+ if (!match) {
104
+ log(client, 'warn', `[Vicoa] Unknown agent "${nextAgent}". Available: ${validNames.join(', ')}`);
105
+ await vicoaClient.sendMessage(`Unknown agent "${nextAgent}". Available agents: ${validNames.join(', ')}`);
106
+ return true;
107
+ }
108
+ // Use the canonical casing returned by OpenCode
109
+ selectedAgent = match;
110
+ setPreferredAgent(match);
111
+ // Cycle the TUI indicator so the terminal also shows the new agent.
112
+ // session.prompt alone only affects a single message server-side;
113
+ // the TUI's displayed agent is purely client-side state mutated by
114
+ // agent.cycle commands.
115
+ const cycled = await cycleTuiToAgent(client, match, getTuiCurrentAgent());
116
+ if (cycled) {
117
+ setTuiCurrentAgent(match);
118
+ log(client, 'info', `[Vicoa] TUI agent indicator cycled to ${match}`);
119
+ }
120
+ else {
121
+ log(client, 'warn', `[Vicoa] Could not cycle TUI to ${match}, will still route messages via session.prompt`);
122
+ }
123
+ }
124
+ catch (error) {
125
+ // If agent listing fails, accept the value optimistically
126
+ log(client, 'warn', `[Vicoa] Could not list agents for validation: ${error}`);
127
+ setPreferredAgent(nextAgent);
128
+ }
129
+ await vicoaClient.sendMessage(`Agent changed to ${selectedAgent}.`);
130
+ return true;
131
+ }
132
+ log(client, 'warn', `[Vicoa] Unknown control command: ${setting}`);
133
+ return true;
134
+ }
135
+ }
136
+ catch {
137
+ // Not a JSON control command, treat as regular message
138
+ }
139
+ return false;
140
+ }
@@ -0,0 +1,27 @@
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;
@@ -0,0 +1,56 @@
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
+ }
@@ -0,0 +1,5 @@
1
+ import type { VicoaClient } from './vicoa-client.js';
2
+ export declare function syncProjectFiles(vicoaClient: VicoaClient, projectPath: string): Promise<void>;
3
+ /**
4
+ * Sync project files to Vicoa backend for @ mentions
5
+ */
@@ -0,0 +1,187 @@
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
+ // }
@@ -0,0 +1,10 @@
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 {};