@syntero/orca-cli 1.3.2 → 1.3.3
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/dist/assistant/anthropic.js +1 -1
- package/dist/assistant/anthropic.js.map +1 -1
- package/dist/assistant/helpers.js +1 -1
- package/dist/assistant/helpers.js.map +1 -1
- package/dist/assistant/openai.js +1 -1
- package/dist/assistant/openai.js.map +1 -1
- package/dist/assistant/prompts.d.ts.map +1 -1
- package/dist/assistant/prompts.js +41 -34
- package/dist/assistant/prompts.js.map +1 -1
- package/dist/assistant/types.d.ts +1 -1
- package/dist/assistant/types.d.ts.map +1 -1
- package/dist/tools/auth-tools.d.ts +26 -0
- package/dist/tools/auth-tools.d.ts.map +1 -0
- package/dist/tools/auth-tools.js +53 -0
- package/dist/tools/auth-tools.js.map +1 -0
- package/dist/tools/command.d.ts +28 -0
- package/dist/tools/command.d.ts.map +1 -0
- package/dist/tools/command.js +76 -0
- package/dist/tools/command.js.map +1 -0
- package/dist/tools/database.d.ts +19 -0
- package/dist/tools/database.d.ts.map +1 -0
- package/dist/tools/database.js +90 -0
- package/dist/tools/database.js.map +1 -0
- package/dist/tools/deployment.d.ts +195 -0
- package/dist/tools/deployment.d.ts.map +1 -0
- package/dist/tools/deployment.js +324 -0
- package/dist/tools/deployment.js.map +1 -0
- package/dist/tools/docker.d.ts +51 -0
- package/dist/tools/docker.d.ts.map +1 -0
- package/dist/tools/docker.js +68 -0
- package/dist/tools/docker.js.map +1 -0
- package/dist/tools/env.d.ts +18 -0
- package/dist/tools/env.d.ts.map +1 -0
- package/dist/tools/env.js +52 -0
- package/dist/tools/env.js.map +1 -0
- package/dist/tools/filesystem.d.ts +77 -0
- package/dist/tools/filesystem.d.ts.map +1 -0
- package/dist/tools/filesystem.js +138 -0
- package/dist/tools/filesystem.js.map +1 -0
- package/dist/tools/handler.d.ts +5 -0
- package/dist/tools/handler.d.ts.map +1 -0
- package/dist/tools/handler.js +51 -0
- package/dist/tools/handler.js.map +1 -0
- package/dist/tools/index.d.ts +462 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +38 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/shared.d.ts +21 -0
- package/dist/tools/shared.d.ts.map +1 -0
- package/dist/tools/shared.js +75 -0
- package/dist/tools/shared.js.map +1 -0
- package/dist/tools/types.d.ts +2 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +3 -0
- package/dist/tools/types.js.map +1 -0
- package/package.json +1 -1
- package/dist/tools.d.ts +0 -908
- package/dist/tools.d.ts.map +0 -1
- package/dist/tools.js +0 -774
- package/dist/tools.js.map +0 -1
package/dist/tools.js
DELETED
|
@@ -1,774 +0,0 @@
|
|
|
1
|
-
import { exec } from 'child_process';
|
|
2
|
-
import { promisify } from 'util';
|
|
3
|
-
const execAsync = promisify(exec);
|
|
4
|
-
import * as fs from 'fs';
|
|
5
|
-
import * as path from 'path';
|
|
6
|
-
import Database from 'better-sqlite3';
|
|
7
|
-
import { getDeploymentDir, saveDeploymentDir, getStoredDeploymentDir } from './settings.js';
|
|
8
|
-
import { redactSecrets, isSecretKey } from './utils.js';
|
|
9
|
-
import { dockerLoginGHCR, listDeploymentFiles, downloadDeploymentFile, isAuthenticated, loadCredentials, getSelectedCloudProvider, cloudRequest, } from './auth.js';
|
|
10
|
-
// Re-export dangerous command detection for use by other modules
|
|
11
|
-
export { detectDangerousCommand, checkCommand, allCategoriesApproved, } from './confirmation/index.js';
|
|
12
|
-
// Tool definitions for Anthropic format
|
|
13
|
-
export const TOOLS_ANTHROPIC = [
|
|
14
|
-
{
|
|
15
|
-
name: 'run_command',
|
|
16
|
-
description: `Execute a shell command and return output. Use for:
|
|
17
|
-
- Docker commands: docker ps, docker logs, docker inspect, docker stats, docker network ls
|
|
18
|
-
- System info: df -h, free -m, uname -a, whoami, id
|
|
19
|
-
- File listing: ls -la <path>
|
|
20
|
-
- Process info: ps aux | grep <pattern>
|
|
21
|
-
- Network: netstat -tlnp, curl -I <url>
|
|
22
|
-
|
|
23
|
-
Commands run with a 30-second timeout. For docker logs, use --tail to limit output.`,
|
|
24
|
-
input_schema: {
|
|
25
|
-
type: 'object',
|
|
26
|
-
properties: {
|
|
27
|
-
command: { type: 'string', description: 'The shell command to execute' },
|
|
28
|
-
gist: { type: 'string', description: 'A human-readable explanation of what this command does, written in 2-3 sentences. Explain the purpose, what it affects, and any important side effects. Example: "This command removes all Docker volumes with \'orca\' in the name. Any data stored in these volumes will be permanently deleted. This is typically used for a clean reinstall."' },
|
|
29
|
-
},
|
|
30
|
-
required: ['command', 'gist'],
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
name: 'read_file',
|
|
35
|
-
description: `Read the contents of a file. Use for config files, logs, etc.
|
|
36
|
-
Paths can be absolute or relative to the deployment directory.
|
|
37
|
-
Do not read .env directly - use inspect_env instead.`,
|
|
38
|
-
input_schema: {
|
|
39
|
-
type: 'object',
|
|
40
|
-
properties: {
|
|
41
|
-
path: { type: 'string', description: 'Path to the file' },
|
|
42
|
-
tail: { type: 'integer', description: 'Only return the last N lines' },
|
|
43
|
-
},
|
|
44
|
-
required: ['path'],
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
name: 'list_directory',
|
|
49
|
-
description: 'List contents of a directory with details.',
|
|
50
|
-
input_schema: {
|
|
51
|
-
type: 'object',
|
|
52
|
-
properties: {
|
|
53
|
-
path: { type: 'string', description: 'Directory path' },
|
|
54
|
-
},
|
|
55
|
-
required: ['path'],
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
name: 'inspect_env',
|
|
60
|
-
description: 'Safely inspect .env configuration with secrets redacted.',
|
|
61
|
-
input_schema: {
|
|
62
|
-
type: 'object',
|
|
63
|
-
properties: {
|
|
64
|
-
show_all: { type: 'boolean', description: 'Show all variables (secrets still redacted)' },
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
name: 'query_database',
|
|
70
|
-
description: 'Execute a READ-ONLY SQL query on the main SQLite database. Only SELECT allowed.',
|
|
71
|
-
input_schema: {
|
|
72
|
-
type: 'object',
|
|
73
|
-
properties: {
|
|
74
|
-
query: { type: 'string', description: 'SQL SELECT query to execute' },
|
|
75
|
-
},
|
|
76
|
-
required: ['query'],
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
name: 'search_logs',
|
|
81
|
-
description: 'Search through docker logs for a pattern.',
|
|
82
|
-
input_schema: {
|
|
83
|
-
type: 'object',
|
|
84
|
-
properties: {
|
|
85
|
-
container: { type: 'string', description: 'Container name' },
|
|
86
|
-
pattern: { type: 'string', description: 'Grep pattern to search for' },
|
|
87
|
-
case_insensitive: { type: 'boolean', description: 'Case insensitive search' },
|
|
88
|
-
context_lines: { type: 'integer', description: 'Context lines before/after match' },
|
|
89
|
-
},
|
|
90
|
-
required: ['container', 'pattern'],
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
name: 'check_container_health',
|
|
95
|
-
description: 'Get a comprehensive health report for Orca containers.',
|
|
96
|
-
input_schema: {
|
|
97
|
-
type: 'object',
|
|
98
|
-
properties: {
|
|
99
|
-
container: { type: 'string', description: "Container name or 'all'" },
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
name: 'grep_file',
|
|
105
|
-
description: `Search for a pattern in a file or directory using grep.
|
|
106
|
-
Use for searching through:
|
|
107
|
-
- Log files for errors or patterns
|
|
108
|
-
- Config files for specific settings
|
|
109
|
-
- Documentation like TROUBLESHOOTING.md for known issues
|
|
110
|
-
- Any text file
|
|
111
|
-
|
|
112
|
-
Returns matching lines with line numbers and optional context.`,
|
|
113
|
-
input_schema: {
|
|
114
|
-
type: 'object',
|
|
115
|
-
properties: {
|
|
116
|
-
pattern: { type: 'string', description: 'Regex pattern to search for' },
|
|
117
|
-
path: { type: 'string', description: 'File or directory path to search' },
|
|
118
|
-
case_insensitive: { type: 'boolean', description: 'Case insensitive search (default: true)' },
|
|
119
|
-
context_lines: { type: 'integer', description: 'Lines of context before and after each match' },
|
|
120
|
-
},
|
|
121
|
-
required: ['pattern', 'path'],
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
name: 'docker_login_ghcr',
|
|
126
|
-
description: `Authenticate with GitHub Container Registry (ghcr.io) to pull Orca images.
|
|
127
|
-
This tool fetches GHCR credentials from the server and runs docker login.
|
|
128
|
-
IMPORTANT: The user must be authenticated (/token) before this will work.
|
|
129
|
-
Use this before pulling Orca images during installation.`,
|
|
130
|
-
input_schema: {
|
|
131
|
-
type: 'object',
|
|
132
|
-
properties: {},
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
name: 'download_deployment_files',
|
|
137
|
-
description: `Download Orca deployment files from the server for local installation.
|
|
138
|
-
Downloads docker-compose.yml, .env.example, and other necessary configuration files.
|
|
139
|
-
IMPORTANT: The user must be authenticated (/token) before this will work.
|
|
140
|
-
Files are saved to the specified target directory.`,
|
|
141
|
-
input_schema: {
|
|
142
|
-
type: 'object',
|
|
143
|
-
properties: {
|
|
144
|
-
target_dir: { type: 'string', description: 'Directory to save deployment files (e.g., ~/orca or /opt/orca)' },
|
|
145
|
-
},
|
|
146
|
-
required: ['target_dir'],
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
name: 'check_auth_status',
|
|
151
|
-
description: 'Check if the user is authenticated with the Orca cloud platform. Returns authentication status, user email, organization ID (org_id), and the cloud provider being used. IMPORTANT: When creating users with manage_users.py, always use the org_id from this tool to ensure correct organization assignment.',
|
|
152
|
-
input_schema: {
|
|
153
|
-
type: 'object',
|
|
154
|
-
properties: {},
|
|
155
|
-
required: [],
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
name: 'save_deployment_dir',
|
|
160
|
-
description: `Save the Orca deployment directory path to settings.
|
|
161
|
-
Use this after successful installation to remember where Orca is installed.
|
|
162
|
-
This allows future update commands to find the installation automatically.`,
|
|
163
|
-
input_schema: {
|
|
164
|
-
type: 'object',
|
|
165
|
-
properties: {
|
|
166
|
-
path: { type: 'string', description: 'Absolute path to the Orca deployment directory (e.g., /home/user/orca or /opt/orca)' },
|
|
167
|
-
},
|
|
168
|
-
required: ['path'],
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
name: 'get_deployment_dir',
|
|
173
|
-
description: `Get the stored Orca deployment directory path from settings.
|
|
174
|
-
Use this to find where Orca was previously installed.
|
|
175
|
-
Returns the path if set, or a message indicating it's not set.
|
|
176
|
-
If not set, ask the user for the deployment path.`,
|
|
177
|
-
input_schema: {
|
|
178
|
-
type: 'object',
|
|
179
|
-
properties: {},
|
|
180
|
-
required: [],
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
name: 'log_deployment_event',
|
|
185
|
-
description: `Log a deployment event (install, update, or rollback) to release_history.json in the deployment directory.
|
|
186
|
-
Creates the file if it doesn't exist. Entries are stored newest-first.
|
|
187
|
-
Call this after every successful or failed install/update/rollback.`,
|
|
188
|
-
input_schema: {
|
|
189
|
-
type: 'object',
|
|
190
|
-
properties: {
|
|
191
|
-
event: { type: 'string', enum: ['install', 'update', 'rollback'], description: 'Type of deployment event' },
|
|
192
|
-
version: { type: 'string', description: 'The version being deployed (e.g., "1.3.0" or "latest")' },
|
|
193
|
-
previous_version: { type: 'string', description: 'The version before this change (for updates/rollbacks)' },
|
|
194
|
-
status: { type: 'string', enum: ['success', 'failed', 'partial'], description: 'Outcome of the deployment' },
|
|
195
|
-
notes: { type: 'string', description: 'Free-text notes: issues encountered, manual steps taken, etc.' },
|
|
196
|
-
},
|
|
197
|
-
required: ['event', 'version', 'status'],
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
name: 'get_deployment_history',
|
|
202
|
-
description: `Read the deployment release history from release_history.json.
|
|
203
|
-
Returns recent deployment events (install, update, rollback) with timestamps, versions, and notes.
|
|
204
|
-
Use this before updates to identify the current version, or to review past deployments.`,
|
|
205
|
-
input_schema: {
|
|
206
|
-
type: 'object',
|
|
207
|
-
properties: {
|
|
208
|
-
limit: { type: 'integer', description: 'Maximum number of entries to return (default: 10)' },
|
|
209
|
-
},
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
name: 'recreate_containers',
|
|
214
|
-
description: `Recreate ALL containers (sandbox, runtime-data, neo4j) for a single organization.
|
|
215
|
-
Removes and recreates all containers fresh. Preserves volumes (user data).
|
|
216
|
-
Call this once per organization after updating Orca to pick up the new images.
|
|
217
|
-
IMPORTANT: The user must be authenticated (/token) before this will work.`,
|
|
218
|
-
input_schema: {
|
|
219
|
-
type: 'object',
|
|
220
|
-
properties: {
|
|
221
|
-
org_id: {
|
|
222
|
-
type: 'string',
|
|
223
|
-
description: 'The organization ID to recreate containers for.',
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
required: ['org_id'],
|
|
227
|
-
},
|
|
228
|
-
},
|
|
229
|
-
];
|
|
230
|
-
// Convert to OpenAI format
|
|
231
|
-
export const TOOLS_OPENAI = TOOLS_ANTHROPIC.map((tool) => ({
|
|
232
|
-
type: 'function',
|
|
233
|
-
function: {
|
|
234
|
-
name: tool.name,
|
|
235
|
-
description: tool.description,
|
|
236
|
-
parameters: tool.input_schema,
|
|
237
|
-
},
|
|
238
|
-
}));
|
|
239
|
-
/**
|
|
240
|
-
* Resolve a path relative to deployment directory.
|
|
241
|
-
*/
|
|
242
|
-
function resolvePath(filePath) {
|
|
243
|
-
const p = path.isAbsolute(filePath) ? filePath : path.join(getDeploymentDir(), filePath);
|
|
244
|
-
return p;
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Execute a shell command with timeout.
|
|
248
|
-
*
|
|
249
|
-
* Note: Dangerous command detection is now handled at a higher level
|
|
250
|
-
* (assistant.ts/ChatApp.tsx) via the confirmation system. This function
|
|
251
|
-
* executes commands directly - callers are responsible for confirming
|
|
252
|
-
* dangerous commands before calling this.
|
|
253
|
-
*/
|
|
254
|
-
export async function runCommand(command, timeout = 30000, signal) {
|
|
255
|
-
try {
|
|
256
|
-
const { stdout, stderr } = await execAsync(command, {
|
|
257
|
-
timeout,
|
|
258
|
-
encoding: 'utf-8',
|
|
259
|
-
cwd: getDeploymentDir(),
|
|
260
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
261
|
-
signal,
|
|
262
|
-
});
|
|
263
|
-
let output = (stdout || '') + (stderr || '');
|
|
264
|
-
if (output.length > 50000) {
|
|
265
|
-
output = output.slice(0, 50000) + '\n... [truncated]';
|
|
266
|
-
}
|
|
267
|
-
return redactSecrets(output) || '(no output)';
|
|
268
|
-
}
|
|
269
|
-
catch (e) {
|
|
270
|
-
// Re-throw abort errors so they propagate up to end the turn
|
|
271
|
-
if (e instanceof Error && e.name === 'AbortError') {
|
|
272
|
-
throw e;
|
|
273
|
-
}
|
|
274
|
-
if (e && typeof e === 'object' && 'killed' in e && e.killed) {
|
|
275
|
-
return `Error: Command timed out after ${timeout / 1000} seconds`;
|
|
276
|
-
}
|
|
277
|
-
if (e && typeof e === 'object' && 'stderr' in e && 'stdout' in e) {
|
|
278
|
-
const execError = e;
|
|
279
|
-
const stderr = execError.stderr
|
|
280
|
-
? typeof execError.stderr === 'string'
|
|
281
|
-
? execError.stderr
|
|
282
|
-
: execError.stderr.toString()
|
|
283
|
-
: '';
|
|
284
|
-
const stdout = execError.stdout
|
|
285
|
-
? typeof execError.stdout === 'string'
|
|
286
|
-
? execError.stdout
|
|
287
|
-
: execError.stdout.toString()
|
|
288
|
-
: '';
|
|
289
|
-
const output = stdout + stderr;
|
|
290
|
-
return redactSecrets(output) || `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
291
|
-
}
|
|
292
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
/**
|
|
296
|
-
* Read a file's contents.
|
|
297
|
-
*/
|
|
298
|
-
export function readFile(filePath, tail) {
|
|
299
|
-
const resolved = resolvePath(filePath);
|
|
300
|
-
if (path.basename(resolved) === '.env' && !resolved.includes('.template')) {
|
|
301
|
-
return 'Error: Use inspect_env tool to safely view environment variables.';
|
|
302
|
-
}
|
|
303
|
-
try {
|
|
304
|
-
let content = fs.readFileSync(resolved, 'utf-8');
|
|
305
|
-
if (tail) {
|
|
306
|
-
const lines = content.split('\n');
|
|
307
|
-
content = lines.slice(-tail).join('\n');
|
|
308
|
-
}
|
|
309
|
-
if (content.length > 100000) {
|
|
310
|
-
content = content.slice(0, 100000) + '\n... [truncated]';
|
|
311
|
-
}
|
|
312
|
-
return content;
|
|
313
|
-
}
|
|
314
|
-
catch (e) {
|
|
315
|
-
if (e && typeof e === 'object' && 'code' in e && e.code === 'ENOENT') {
|
|
316
|
-
return `Error: File not found: ${resolved}`;
|
|
317
|
-
}
|
|
318
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
322
|
-
* Format file mode (permissions) as a string like 'drwxr-xr-x'.
|
|
323
|
-
*/
|
|
324
|
-
function formatMode(mode, isDir, isLink) {
|
|
325
|
-
const typeChar = isLink ? 'l' : isDir ? 'd' : '-';
|
|
326
|
-
const perms = [
|
|
327
|
-
(mode & 0o400) ? 'r' : '-',
|
|
328
|
-
(mode & 0o200) ? 'w' : '-',
|
|
329
|
-
(mode & 0o100) ? 'x' : '-',
|
|
330
|
-
(mode & 0o040) ? 'r' : '-',
|
|
331
|
-
(mode & 0o020) ? 'w' : '-',
|
|
332
|
-
(mode & 0o010) ? 'x' : '-',
|
|
333
|
-
(mode & 0o004) ? 'r' : '-',
|
|
334
|
-
(mode & 0o002) ? 'w' : '-',
|
|
335
|
-
(mode & 0o001) ? 'x' : '-',
|
|
336
|
-
].join('');
|
|
337
|
-
return typeChar + perms;
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* Format file size in human-readable format.
|
|
341
|
-
*/
|
|
342
|
-
function formatSize(size) {
|
|
343
|
-
return size.toString().padStart(8);
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Format date in ls-like format.
|
|
347
|
-
*/
|
|
348
|
-
function formatDate(date) {
|
|
349
|
-
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
350
|
-
const month = months[date.getMonth()];
|
|
351
|
-
const day = date.getDate().toString().padStart(2);
|
|
352
|
-
const hours = date.getHours().toString().padStart(2, '0');
|
|
353
|
-
const mins = date.getMinutes().toString().padStart(2, '0');
|
|
354
|
-
return `${month} ${day} ${hours}:${mins}`;
|
|
355
|
-
}
|
|
356
|
-
/**
|
|
357
|
-
* List directory contents using Node.js fs APIs (cross-platform).
|
|
358
|
-
*/
|
|
359
|
-
export function listDirectory(dirPath) {
|
|
360
|
-
const resolved = resolvePath(dirPath);
|
|
361
|
-
try {
|
|
362
|
-
const stats = fs.statSync(resolved);
|
|
363
|
-
if (!stats.isDirectory()) {
|
|
364
|
-
return `Error: Not a directory: ${resolved}`;
|
|
365
|
-
}
|
|
366
|
-
const entries = fs.readdirSync(resolved, { withFileTypes: true });
|
|
367
|
-
const lines = [];
|
|
368
|
-
lines.push(`total ${entries.length}`);
|
|
369
|
-
// Sort entries: directories first, then alphabetically
|
|
370
|
-
const sortedEntries = entries.sort((a, b) => {
|
|
371
|
-
if (a.isDirectory() && !b.isDirectory())
|
|
372
|
-
return -1;
|
|
373
|
-
if (!a.isDirectory() && b.isDirectory())
|
|
374
|
-
return 1;
|
|
375
|
-
return a.name.localeCompare(b.name);
|
|
376
|
-
});
|
|
377
|
-
for (const entry of sortedEntries) {
|
|
378
|
-
const fullPath = path.join(resolved, entry.name);
|
|
379
|
-
try {
|
|
380
|
-
const entryStats = fs.lstatSync(fullPath);
|
|
381
|
-
const mode = formatMode(entryStats.mode, entry.isDirectory(), entry.isSymbolicLink());
|
|
382
|
-
const size = formatSize(entryStats.size);
|
|
383
|
-
const date = formatDate(entryStats.mtime);
|
|
384
|
-
const name = entry.isSymbolicLink()
|
|
385
|
-
? `${entry.name} -> ${fs.readlinkSync(fullPath)}`
|
|
386
|
-
: entry.name;
|
|
387
|
-
lines.push(`${mode} ${size} ${date} ${name}`);
|
|
388
|
-
}
|
|
389
|
-
catch {
|
|
390
|
-
// If we can't stat the entry, show minimal info
|
|
391
|
-
lines.push(`?????????? ? ??? ?? ??:?? ${entry.name}`);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
return lines.join('\n');
|
|
395
|
-
}
|
|
396
|
-
catch (e) {
|
|
397
|
-
if (e && typeof e === 'object' && 'code' in e && e.code === 'ENOENT') {
|
|
398
|
-
return `Error: Directory not found: ${resolved}`;
|
|
399
|
-
}
|
|
400
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Inspect .env file with secrets redacted.
|
|
405
|
-
*/
|
|
406
|
-
export function inspectEnv(showAll = false) {
|
|
407
|
-
const envPath = path.join(getDeploymentDir(), '.env');
|
|
408
|
-
if (!fs.existsSync(envPath)) {
|
|
409
|
-
return `No .env file found at ${envPath}`;
|
|
410
|
-
}
|
|
411
|
-
const result = [];
|
|
412
|
-
const content = fs.readFileSync(envPath, 'utf-8');
|
|
413
|
-
for (const line of content.split('\n')) {
|
|
414
|
-
const trimmed = line.trim();
|
|
415
|
-
if (!trimmed || trimmed.startsWith('#')) {
|
|
416
|
-
result.push(trimmed);
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
if (trimmed.includes('=')) {
|
|
420
|
-
const [key, ...valueParts] = trimmed.split('=');
|
|
421
|
-
const value = valueParts.join('=').trim();
|
|
422
|
-
const keyTrimmed = key.trim();
|
|
423
|
-
if (isSecretKey(keyTrimmed)) {
|
|
424
|
-
if (value) {
|
|
425
|
-
result.push(`${keyTrimmed}=[REDACTED - ${value.length} chars]`);
|
|
426
|
-
}
|
|
427
|
-
else {
|
|
428
|
-
result.push(`${keyTrimmed}=[NOT SET]`);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
else if (showAll || !isSecretKey(keyTrimmed)) {
|
|
432
|
-
result.push(`${keyTrimmed}=${value}`);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
return result.join('\n');
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Find the SQLite database path.
|
|
440
|
-
*/
|
|
441
|
-
async function findDatabasePath() {
|
|
442
|
-
const deploymentDir = getDeploymentDir();
|
|
443
|
-
const candidates = [
|
|
444
|
-
path.join(deploymentDir, 'data', 'main.db'),
|
|
445
|
-
path.join(deploymentDir, 'backend-data', 'main.db'),
|
|
446
|
-
];
|
|
447
|
-
try {
|
|
448
|
-
const { stdout } = await execAsync('docker volume inspect backend-data --format "{{.Mountpoint}}"', { encoding: 'utf-8', timeout: 5000 });
|
|
449
|
-
const result = stdout.trim();
|
|
450
|
-
if (result) {
|
|
451
|
-
candidates.unshift(path.join(result, 'main.db'));
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
catch {
|
|
455
|
-
// Ignore errors
|
|
456
|
-
}
|
|
457
|
-
for (const dbPath of candidates) {
|
|
458
|
-
if (fs.existsSync(dbPath)) {
|
|
459
|
-
return dbPath;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
return null;
|
|
463
|
-
}
|
|
464
|
-
/**
|
|
465
|
-
* Execute a read-only SQL query.
|
|
466
|
-
*/
|
|
467
|
-
export async function queryDatabase(query, signal) {
|
|
468
|
-
const queryUpper = query.trim().toUpperCase();
|
|
469
|
-
if (!queryUpper.startsWith('SELECT')) {
|
|
470
|
-
return 'Error: Only SELECT queries are allowed.';
|
|
471
|
-
}
|
|
472
|
-
const dangerous = ['DROP', 'DELETE', 'INSERT', 'UPDATE', 'ALTER', 'CREATE', 'TRUNCATE'];
|
|
473
|
-
for (const d of dangerous) {
|
|
474
|
-
if (queryUpper.includes(d)) {
|
|
475
|
-
return `Error: Query contains blocked keyword: ${d}`;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
const dbPath = await findDatabasePath();
|
|
479
|
-
if (!dbPath) {
|
|
480
|
-
// Try via docker
|
|
481
|
-
return runCommand(`docker exec orca-backend sqlite3 /app/backend/data/main.db "${query}"`, undefined, signal);
|
|
482
|
-
}
|
|
483
|
-
try {
|
|
484
|
-
const db = new Database(dbPath, { readonly: true });
|
|
485
|
-
const stmt = db.prepare(query);
|
|
486
|
-
const rows = stmt.all();
|
|
487
|
-
if (rows.length === 0) {
|
|
488
|
-
db.close();
|
|
489
|
-
return '(no results)';
|
|
490
|
-
}
|
|
491
|
-
const columns = Object.keys(rows[0]);
|
|
492
|
-
const result = [columns.join('\t'), '-'.repeat(40)];
|
|
493
|
-
for (const row of rows.slice(0, 100)) {
|
|
494
|
-
result.push(columns.map((col) => String(row[col])).join('\t'));
|
|
495
|
-
}
|
|
496
|
-
if (rows.length > 100) {
|
|
497
|
-
result.push(`... (${rows.length - 100} more rows)`);
|
|
498
|
-
}
|
|
499
|
-
db.close();
|
|
500
|
-
return result.join('\n');
|
|
501
|
-
}
|
|
502
|
-
catch (e) {
|
|
503
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* Search container logs for a pattern.
|
|
508
|
-
*/
|
|
509
|
-
export async function searchLogs(container, pattern, caseInsensitive = true, contextLines = 2, signal) {
|
|
510
|
-
let grepFlags = caseInsensitive ? '-i' : '';
|
|
511
|
-
if (contextLines) {
|
|
512
|
-
grepFlags += ` -C ${contextLines}`;
|
|
513
|
-
}
|
|
514
|
-
return runCommand(`docker logs ${container} 2>&1 | grep ${grepFlags} "${pattern}" | tail -100`, undefined, signal);
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* Search for a pattern in a file or directory using grep.
|
|
518
|
-
*/
|
|
519
|
-
export async function grepFile(pattern, filePath, caseInsensitive = true, contextLines = 0, signal) {
|
|
520
|
-
const resolved = resolvePath(filePath);
|
|
521
|
-
const flags = [
|
|
522
|
-
'-n', // line numbers
|
|
523
|
-
caseInsensitive ? '-i' : '',
|
|
524
|
-
contextLines > 0 ? `-C ${contextLines}` : '',
|
|
525
|
-
].filter(Boolean).join(' ');
|
|
526
|
-
return runCommand(`grep ${flags} -E "${pattern}" "${resolved}"`, undefined, signal);
|
|
527
|
-
}
|
|
528
|
-
/**
|
|
529
|
-
* Check health of Orca containers.
|
|
530
|
-
*/
|
|
531
|
-
export async function checkContainerHealth(container = 'all', signal) {
|
|
532
|
-
const result = [];
|
|
533
|
-
let containers;
|
|
534
|
-
if (container === 'all') {
|
|
535
|
-
containers = ['orca-backend', 'orca-frontend', 'orca-redis'];
|
|
536
|
-
const psResult = await runCommand('docker ps -a --filter "name=orca-" --format "{{.Names}}"', undefined, signal);
|
|
537
|
-
for (const name of psResult.trim().split('\n')) {
|
|
538
|
-
if (name && !containers.includes(name)) {
|
|
539
|
-
containers.push(name);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
else {
|
|
544
|
-
containers = [container];
|
|
545
|
-
}
|
|
546
|
-
for (const c of containers) {
|
|
547
|
-
result.push(`\n${'='.repeat(60)}\nContainer: ${c}\n${'='.repeat(60)}`);
|
|
548
|
-
result.push(await runCommand(`docker inspect ${c} --format "Status: {{.State.Status}}, Restarts: {{.RestartCount}}"`, undefined, signal));
|
|
549
|
-
const stats = await runCommand(`docker stats ${c} --no-stream --format "CPU: {{.CPUPerc}}, Mem: {{.MemUsage}}"`, undefined, signal);
|
|
550
|
-
if (!stats.includes('Error')) {
|
|
551
|
-
result.push(stats);
|
|
552
|
-
}
|
|
553
|
-
result.push('\nRecent logs (last 10 lines):');
|
|
554
|
-
result.push(await runCommand(`docker logs ${c} --tail 10 2>&1`, undefined, signal));
|
|
555
|
-
}
|
|
556
|
-
return result.join('\n');
|
|
557
|
-
}
|
|
558
|
-
/**
|
|
559
|
-
* Log a deployment event to release_history.json.
|
|
560
|
-
*/
|
|
561
|
-
export function logDeploymentEvent(event, version, status, previousVersion, notes) {
|
|
562
|
-
const historyPath = path.join(getDeploymentDir(), 'release_history.json');
|
|
563
|
-
let history = [];
|
|
564
|
-
if (fs.existsSync(historyPath)) {
|
|
565
|
-
try {
|
|
566
|
-
history = JSON.parse(fs.readFileSync(historyPath, 'utf-8'));
|
|
567
|
-
}
|
|
568
|
-
catch {
|
|
569
|
-
history = [];
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
const entry = {
|
|
573
|
-
timestamp: new Date().toISOString(),
|
|
574
|
-
event,
|
|
575
|
-
version,
|
|
576
|
-
previous_version: previousVersion || null,
|
|
577
|
-
status,
|
|
578
|
-
notes: notes || null,
|
|
579
|
-
};
|
|
580
|
-
history.unshift(entry);
|
|
581
|
-
fs.writeFileSync(historyPath, JSON.stringify(history, null, 2), 'utf-8');
|
|
582
|
-
return `Logged ${event} event: version=${version}, status=${status}`;
|
|
583
|
-
}
|
|
584
|
-
/**
|
|
585
|
-
* Read deployment history from release_history.json.
|
|
586
|
-
*/
|
|
587
|
-
export function getDeploymentHistory(limit = 10) {
|
|
588
|
-
const historyPath = path.join(getDeploymentDir(), 'release_history.json');
|
|
589
|
-
if (!fs.existsSync(historyPath)) {
|
|
590
|
-
return 'No deployment history found. No installs, updates, or rollbacks have been recorded yet.';
|
|
591
|
-
}
|
|
592
|
-
let history;
|
|
593
|
-
try {
|
|
594
|
-
history = JSON.parse(fs.readFileSync(historyPath, 'utf-8'));
|
|
595
|
-
}
|
|
596
|
-
catch {
|
|
597
|
-
return 'Error: Could not parse release_history.json.';
|
|
598
|
-
}
|
|
599
|
-
if (history.length === 0) {
|
|
600
|
-
return 'Deployment history is empty.';
|
|
601
|
-
}
|
|
602
|
-
const entries = history.slice(0, limit);
|
|
603
|
-
const lines = entries.map((e, i) => {
|
|
604
|
-
const parts = [
|
|
605
|
-
`${i + 1}. [${e.timestamp}] ${String(e.event).toUpperCase()} → v${e.version}`,
|
|
606
|
-
` Status: ${e.status}`,
|
|
607
|
-
];
|
|
608
|
-
if (e.previous_version) {
|
|
609
|
-
parts.push(` Previous: v${e.previous_version}`);
|
|
610
|
-
}
|
|
611
|
-
if (e.notes) {
|
|
612
|
-
parts.push(` Notes: ${e.notes}`);
|
|
613
|
-
}
|
|
614
|
-
return parts.join('\n');
|
|
615
|
-
});
|
|
616
|
-
const header = `Deployment History (showing ${entries.length} of ${history.length}):`;
|
|
617
|
-
return [header, '', ...lines].join('\n');
|
|
618
|
-
}
|
|
619
|
-
/**
|
|
620
|
-
* Handle downloading deployment files.
|
|
621
|
-
*/
|
|
622
|
-
async function handleDownloadDeploymentFiles(targetDir) {
|
|
623
|
-
// Expand ~ to home directory
|
|
624
|
-
const expandedDir = targetDir.replace(/^~/, process.env.HOME || process.env.USERPROFILE || '');
|
|
625
|
-
const resolvedDir = path.resolve(expandedDir);
|
|
626
|
-
// Get list of available files
|
|
627
|
-
const files = await listDeploymentFiles();
|
|
628
|
-
if (!files || files.length === 0) {
|
|
629
|
-
return 'Error: Could not fetch deployment files list from server. Make sure you are authenticated (/token).';
|
|
630
|
-
}
|
|
631
|
-
// Create target directory if it doesn't exist
|
|
632
|
-
if (!fs.existsSync(resolvedDir)) {
|
|
633
|
-
fs.mkdirSync(resolvedDir, { recursive: true });
|
|
634
|
-
}
|
|
635
|
-
const results = [];
|
|
636
|
-
let successCount = 0;
|
|
637
|
-
let failCount = 0;
|
|
638
|
-
for (const filename of files) {
|
|
639
|
-
const targetPath = path.join(resolvedDir, filename);
|
|
640
|
-
const success = await downloadDeploymentFile(filename, targetPath);
|
|
641
|
-
if (success) {
|
|
642
|
-
results.push(`✓ ${filename}`);
|
|
643
|
-
successCount++;
|
|
644
|
-
}
|
|
645
|
-
else {
|
|
646
|
-
results.push(`✗ ${filename} (failed)`);
|
|
647
|
-
failCount++;
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
const summary = failCount === 0
|
|
651
|
-
? `Successfully downloaded ${successCount} deployment files to ${resolvedDir}`
|
|
652
|
-
: `Downloaded ${successCount} files, ${failCount} failed`;
|
|
653
|
-
return `${summary}\n\nFiles:\n${results.join('\n')}\n\nNext steps:\n1. cd ${resolvedDir}\n2. cp .env.example .env\n3. Edit .env with your configuration\n4. docker compose up -d`;
|
|
654
|
-
}
|
|
655
|
-
/**
|
|
656
|
-
* Handle a tool call and return the result.
|
|
657
|
-
*/
|
|
658
|
-
export async function handleToolCall(name, inputData, signal) {
|
|
659
|
-
// Async handlers
|
|
660
|
-
if (name === 'docker_login_ghcr') {
|
|
661
|
-
const success = await dockerLoginGHCR();
|
|
662
|
-
return success
|
|
663
|
-
? 'Successfully authenticated with GHCR. You can now pull Orca images.'
|
|
664
|
-
: 'Failed to authenticate with GHCR. Make sure you are authenticated (/token) and the server has GHCR configured.';
|
|
665
|
-
}
|
|
666
|
-
if (name === 'download_deployment_files') {
|
|
667
|
-
return handleDownloadDeploymentFiles(inputData.target_dir);
|
|
668
|
-
}
|
|
669
|
-
if (name === 'check_auth_status') {
|
|
670
|
-
const authenticated = isAuthenticated();
|
|
671
|
-
if (!authenticated) {
|
|
672
|
-
return JSON.stringify({
|
|
673
|
-
authenticated: false,
|
|
674
|
-
message: 'Not authenticated. Use /token command to authenticate.',
|
|
675
|
-
});
|
|
676
|
-
}
|
|
677
|
-
const creds = loadCredentials();
|
|
678
|
-
const provider = getSelectedCloudProvider();
|
|
679
|
-
return JSON.stringify({
|
|
680
|
-
authenticated: true,
|
|
681
|
-
email: creds?.user?.email || 'unknown',
|
|
682
|
-
org_id: creds?.org_id || 'unknown',
|
|
683
|
-
provider: provider || 'none',
|
|
684
|
-
});
|
|
685
|
-
}
|
|
686
|
-
if (name === 'save_deployment_dir') {
|
|
687
|
-
const deployPath = inputData.path;
|
|
688
|
-
// Expand ~ to home directory
|
|
689
|
-
const expandedPath = deployPath.replace(/^~/, process.env.HOME || process.env.USERPROFILE || '');
|
|
690
|
-
const resolvedPath = path.resolve(expandedPath);
|
|
691
|
-
// Verify the directory exists and has docker-compose.yml
|
|
692
|
-
const composePath = path.join(resolvedPath, 'docker-compose.yml');
|
|
693
|
-
if (!fs.existsSync(composePath)) {
|
|
694
|
-
return `Error: docker-compose.yml not found at ${resolvedPath}. Please verify the path is correct.`;
|
|
695
|
-
}
|
|
696
|
-
saveDeploymentDir(resolvedPath);
|
|
697
|
-
return `Deployment directory saved: ${resolvedPath}`;
|
|
698
|
-
}
|
|
699
|
-
if (name === 'recreate_containers') {
|
|
700
|
-
if (!isAuthenticated()) {
|
|
701
|
-
return 'Error: Not authenticated. Use /token command to authenticate first.';
|
|
702
|
-
}
|
|
703
|
-
const orgId = String(inputData.org_id ?? '').trim();
|
|
704
|
-
if (!orgId) {
|
|
705
|
-
return 'Error: org_id is required.';
|
|
706
|
-
}
|
|
707
|
-
try {
|
|
708
|
-
// Step 1: Delete all containers for the org
|
|
709
|
-
const delResp = await cloudRequest('DELETE', `/api/v1/super-admin/workspaces/${orgId}?force=true`);
|
|
710
|
-
const deleteOk = delResp.status >= 200 && delResp.status < 300;
|
|
711
|
-
if (!deleteOk) {
|
|
712
|
-
return JSON.stringify({ org_id: orgId, success: false, error: `delete failed (status ${delResp.status}): ${JSON.stringify(delResp.data)}` });
|
|
713
|
-
}
|
|
714
|
-
// Step 2: Recreate containers
|
|
715
|
-
const createResp = await cloudRequest('POST', `/api/v1/super-admin/workspaces/${orgId}`);
|
|
716
|
-
const createOk = createResp.status >= 200 && createResp.status < 300;
|
|
717
|
-
if (!createOk) {
|
|
718
|
-
return JSON.stringify({ org_id: orgId, success: false, delete_ok: true, error: `create failed (status ${createResp.status}): ${JSON.stringify(createResp.data)}` });
|
|
719
|
-
}
|
|
720
|
-
return JSON.stringify({ org_id: orgId, success: true, message: `Containers recreated for ${orgId}.` });
|
|
721
|
-
}
|
|
722
|
-
catch (e) {
|
|
723
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
if (name === 'log_deployment_event') {
|
|
727
|
-
return logDeploymentEvent(inputData.event, inputData.version, inputData.status, inputData.previous_version, inputData.notes);
|
|
728
|
-
}
|
|
729
|
-
if (name === 'get_deployment_history') {
|
|
730
|
-
return getDeploymentHistory(inputData.limit ?? 10);
|
|
731
|
-
}
|
|
732
|
-
if (name === 'get_deployment_dir') {
|
|
733
|
-
const storedDir = getStoredDeploymentDir();
|
|
734
|
-
if (storedDir) {
|
|
735
|
-
// Verify the directory still exists
|
|
736
|
-
const composePath = path.join(storedDir, 'docker-compose.yml');
|
|
737
|
-
if (fs.existsSync(composePath)) {
|
|
738
|
-
return JSON.stringify({
|
|
739
|
-
found: true,
|
|
740
|
-
path: storedDir,
|
|
741
|
-
message: `Deployment directory: ${storedDir}`,
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
else {
|
|
745
|
-
return JSON.stringify({
|
|
746
|
-
found: false,
|
|
747
|
-
stale_path: storedDir,
|
|
748
|
-
message: `Previously stored path ${storedDir} no longer contains docker-compose.yml. Ask user for the correct path.`,
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
return JSON.stringify({
|
|
753
|
-
found: false,
|
|
754
|
-
message: 'Deployment directory not set. Ask user where Orca is installed.',
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
// Tool handlers
|
|
758
|
-
const handlers = {
|
|
759
|
-
run_command: () => runCommand(inputData.command, undefined, signal),
|
|
760
|
-
read_file: () => readFile(inputData.path, inputData.tail),
|
|
761
|
-
list_directory: () => listDirectory(inputData.path),
|
|
762
|
-
inspect_env: () => inspectEnv(inputData.show_all || false),
|
|
763
|
-
query_database: () => queryDatabase(inputData.query, signal),
|
|
764
|
-
search_logs: () => searchLogs(inputData.container, inputData.pattern, inputData.case_insensitive ?? true, inputData.context_lines ?? 2, signal),
|
|
765
|
-
check_container_health: () => checkContainerHealth(inputData.container || 'all', signal),
|
|
766
|
-
grep_file: () => grepFile(inputData.pattern, inputData.path, inputData.case_insensitive ?? true, inputData.context_lines ?? 0, signal),
|
|
767
|
-
};
|
|
768
|
-
const handler = handlers[name];
|
|
769
|
-
if (handler) {
|
|
770
|
-
return handler();
|
|
771
|
-
}
|
|
772
|
-
return `Unknown tool: ${name}`;
|
|
773
|
-
}
|
|
774
|
-
//# sourceMappingURL=tools.js.map
|