@vicoa/opencode 0.1.0 → 0.1.2

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 CHANGED
@@ -75,7 +75,7 @@ npm run build
75
75
  ```json
76
76
  {
77
77
  "$schema": "https://opencode.ai/config.json",
78
- "plugins": ["file://<path/to/opencode-vicoa>"]
78
+ "plugins": ["file://<path/to/opencode-vicoa/dist/index.js>"]
79
79
  }
80
80
  ```
81
81
 
package/dist/index.js CHANGED
@@ -34,6 +34,8 @@ let preferredAgent;
34
34
  // the TUI sent that message with). Used to compute how many agent.cycle
35
35
  // steps are needed to land on a target agent.
36
36
  let tuiCurrentAgent;
37
+ // Track current session status to detect if OpenCode is idle
38
+ let currentSessionStatus;
37
39
  // Track messages that came from the UI (to avoid sending them back)
38
40
  // Keep a simple FIFO buffer of message content
39
41
  const messagesFromUI = [];
@@ -254,6 +256,7 @@ export const VicoaPlugin = async (context) => {
254
256
  client,
255
257
  vicoaClient,
256
258
  currentSessionId,
259
+ currentSessionStatus,
257
260
  getTuiCurrentAgent: () => tuiCurrentAgent,
258
261
  setTuiCurrentAgent: (agent) => {
259
262
  tuiCurrentAgent = agent;
@@ -291,6 +294,8 @@ export const VicoaPlugin = async (context) => {
291
294
  return; // Do NOT forward this message as a prompt
292
295
  }
293
296
  if (await handleSlashCommand(userMessage, client, currentSessionId, vicoaClient)) {
297
+ // After processing a slash command, set status to AWAITING_INPUT
298
+ await vicoaClient.updateStatus('AWAITING_INPUT');
294
299
  return;
295
300
  }
296
301
  // Submit as a prompt via the TUI. Mark it first so the chat.message
@@ -394,6 +399,8 @@ export const VicoaPlugin = async (context) => {
394
399
  }
395
400
  case 'session.status': {
396
401
  const statusType = event.properties.status.type;
402
+ // Track current status for interrupt handling
403
+ currentSessionStatus = statusType;
397
404
  if (statusType === 'busy' || statusType === 'retry') {
398
405
  await vicoaClient.updateStatus('ACTIVE');
399
406
  }
@@ -3,6 +3,7 @@ type ControlCommandContext = {
3
3
  client: any;
4
4
  vicoaClient: VicoaClient;
5
5
  currentSessionId?: string;
6
+ currentSessionStatus?: 'idle' | 'busy' | 'retry';
6
7
  getTuiCurrentAgent: () => string | undefined;
7
8
  setTuiCurrentAgent: (agent: string | undefined) => void;
8
9
  setPreferredAgent: (agent: string | undefined) => void;
@@ -61,7 +61,7 @@ function extractControlPayload(content) {
61
61
  * Handle control commands from Vicoa (matching Claude wrapper pattern)
62
62
  */
63
63
  export async function handleControlCommand(content, context) {
64
- const { client, vicoaClient, currentSessionId, getTuiCurrentAgent, setPreferredAgent, setTuiCurrentAgent } = context;
64
+ const { client, vicoaClient, currentSessionId, currentSessionStatus, getTuiCurrentAgent, setPreferredAgent, setTuiCurrentAgent } = context;
65
65
  // Try to parse as JSON control command
66
66
  try {
67
67
  const controlPayload = extractControlPayload(content);
@@ -74,6 +74,12 @@ export async function handleControlCommand(content, context) {
74
74
  log(client, 'warn', '[Vicoa] Interrupt failed: no active session');
75
75
  return true;
76
76
  }
77
+ // Check if OpenCode is idle
78
+ if (currentSessionStatus === 'idle') {
79
+ await vicoaClient.sendMessage('OpenCode is idle.');
80
+ log(client, 'info', '[Vicoa] OpenCode is already idle, no interrupt needed');
81
+ return true;
82
+ }
77
83
  try {
78
84
  await executeTuiCommand(client, 'session.interrupt');
79
85
  await executeTuiCommand(client, 'session.interrupt');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vicoa/opencode",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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"
@@ -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
- }
@@ -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;
@@ -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
- }
@@ -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 {};