mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
feat: add interactive web terminal for bot containers (#232)
* feat(terminal): add interactive web terminal for bot containers Add WebSocket-based terminal endpoint (/container/terminal/ws) that provides a full PTY shell session inside the bot's MCP container. Extend the gRPC proto with pty and resize fields, implement PTY exec on the container side using creack/pty, and add an xterm.js-based terminal component in the frontend bot detail page. * chore: add /mcp in .gitignore * feat(terminal): add multi-tab support, localStorage cache, and reactivity fixes - Support unlimited terminal tabs with add/close/switch - Cache terminal content to localStorage via SerializeAddon for session persistence - Use shallowReactive for tab objects to ensure status updates trigger UI reactivity - Fix listener leak by tracking and disposing onData/onResize on reconnect - Fix bottom clipping by using inset offsets instead of padding
This commit is contained in:
@@ -93,6 +93,7 @@ tmp/
|
|||||||
# compiled files
|
# compiled files
|
||||||
/memoh
|
/memoh
|
||||||
/agent
|
/agent
|
||||||
|
/mcp
|
||||||
|
|
||||||
docs/docs/.vitepress/cache
|
docs/docs/.vitepress/cache
|
||||||
.pnpm-store
|
.pnpm-store
|
||||||
|
|||||||
@@ -22,6 +22,9 @@
|
|||||||
"@tanstack/vue-table": "^8.21.3",
|
"@tanstack/vue-table": "^8.21.3",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
|
"@xterm/addon-serialize": "^0.14.0",
|
||||||
|
"@xterm/xterm": "^6.0.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"katex": "^0.16.28",
|
"katex": "^0.16.28",
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
const STORAGE_PREFIX = 'terminal-cache:'
|
||||||
|
const MAX_CONTENT_BYTES = 100 * 1024
|
||||||
|
const MAX_CACHED_TABS = 10
|
||||||
|
|
||||||
|
export interface TerminalTabState {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
content: string
|
||||||
|
savedAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TerminalCacheState {
|
||||||
|
tabs: TerminalTabState[]
|
||||||
|
activeTabId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function storageKey(botId: string): string {
|
||||||
|
return `${STORAGE_PREFIX}${botId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateContent(content: string): string {
|
||||||
|
if (content.length <= MAX_CONTENT_BYTES) return content
|
||||||
|
return content.slice(content.length - MAX_CONTENT_BYTES)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTerminalCache() {
|
||||||
|
function loadCache(botId: string): TerminalCacheState | null {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(storageKey(botId))
|
||||||
|
if (!raw) return null
|
||||||
|
const parsed = JSON.parse(raw) as TerminalCacheState
|
||||||
|
if (!Array.isArray(parsed.tabs) || !parsed.activeTabId) return null
|
||||||
|
return parsed
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCache(botId: string, state: TerminalCacheState) {
|
||||||
|
try {
|
||||||
|
const trimmed: TerminalCacheState = {
|
||||||
|
activeTabId: state.activeTabId,
|
||||||
|
tabs: state.tabs.slice(0, MAX_CACHED_TABS).map((tab) => ({
|
||||||
|
id: tab.id,
|
||||||
|
label: tab.label,
|
||||||
|
content: truncateContent(tab.content),
|
||||||
|
savedAt: Date.now(),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
localStorage.setItem(storageKey(botId), JSON.stringify(trimmed))
|
||||||
|
} catch {
|
||||||
|
// localStorage full or unavailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCache(botId: string) {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(storageKey(botId))
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { loadCache, saveCache, clearCache }
|
||||||
|
}
|
||||||
@@ -493,8 +493,22 @@
|
|||||||
"skills": "Skills",
|
"skills": "Skills",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"files": "Files",
|
"files": "Files",
|
||||||
|
"terminal": "Terminal",
|
||||||
"settings": "Settings"
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
|
"terminal": {
|
||||||
|
"title": "Terminal",
|
||||||
|
"reconnect": "Reconnect",
|
||||||
|
"newTab": "New Terminal",
|
||||||
|
"closeTab": "Close",
|
||||||
|
"defaultTabLabel": "Terminal",
|
||||||
|
"status": {
|
||||||
|
"idle": "Idle",
|
||||||
|
"connecting": "Connecting...",
|
||||||
|
"connected": "Connected",
|
||||||
|
"disconnected": "Disconnected"
|
||||||
|
}
|
||||||
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"size": "Size",
|
"size": "Size",
|
||||||
|
|||||||
@@ -489,8 +489,22 @@
|
|||||||
"skills": "技能",
|
"skills": "技能",
|
||||||
"email": "邮件",
|
"email": "邮件",
|
||||||
"files": "文件",
|
"files": "文件",
|
||||||
|
"terminal": "终端",
|
||||||
"settings": "设置"
|
"settings": "设置"
|
||||||
},
|
},
|
||||||
|
"terminal": {
|
||||||
|
"title": "终端",
|
||||||
|
"reconnect": "重新连接",
|
||||||
|
"newTab": "新建终端",
|
||||||
|
"closeTab": "关闭",
|
||||||
|
"defaultTabLabel": "终端",
|
||||||
|
"status": {
|
||||||
|
"idle": "空闲",
|
||||||
|
"connecting": "连接中...",
|
||||||
|
"connected": "已连接",
|
||||||
|
"disconnected": "已断开"
|
||||||
|
}
|
||||||
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"name": "名称",
|
"name": "名称",
|
||||||
"size": "大小",
|
"size": "大小",
|
||||||
|
|||||||
@@ -0,0 +1,458 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, shallowReactive, onMounted, onBeforeUnmount, watch, computed, nextTick } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { Terminal } from '@xterm/xterm'
|
||||||
|
import { FitAddon } from '@xterm/addon-fit'
|
||||||
|
import { SerializeAddon } from '@xterm/addon-serialize'
|
||||||
|
import { client } from '@memoh/sdk/client'
|
||||||
|
import { Button } from '@memoh/ui'
|
||||||
|
import { useSyncedQueryParam } from '@/composables/useSyncedQueryParam'
|
||||||
|
import { useTerminalCache } from '@/composables/useTerminalCache'
|
||||||
|
import type { TerminalCacheState } from '@/composables/useTerminalCache'
|
||||||
|
import '@xterm/xterm/css/xterm.css'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const { loadCache, saveCache } = useTerminalCache()
|
||||||
|
|
||||||
|
const botId = computed(() => route.params.botId as string)
|
||||||
|
const activeTab = useSyncedQueryParam('tab', 'overview')
|
||||||
|
|
||||||
|
const TERMINAL_OPTIONS = {
|
||||||
|
cursorBlink: true,
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
||||||
|
theme: {
|
||||||
|
background: '#1a1b26',
|
||||||
|
foreground: '#a9b1d6',
|
||||||
|
cursor: '#c0caf5',
|
||||||
|
selectionBackground: '#33467c',
|
||||||
|
},
|
||||||
|
} as const
|
||||||
|
|
||||||
|
interface TerminalTab {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
terminal: Terminal | null
|
||||||
|
fitAddon: FitAddon | null
|
||||||
|
serializeAddon: SerializeAddon | null
|
||||||
|
ws: WebSocket | null
|
||||||
|
status: 'idle' | 'connecting' | 'connected' | 'disconnected'
|
||||||
|
containerEl: HTMLDivElement | null
|
||||||
|
wsDisposables: Array<{ dispose(): void }>
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeTab(id: string, label: string): TerminalTab {
|
||||||
|
return shallowReactive<TerminalTab>({
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
terminal: null,
|
||||||
|
fitAddon: null,
|
||||||
|
serializeAddon: null,
|
||||||
|
ws: null,
|
||||||
|
status: 'idle',
|
||||||
|
containerEl: null,
|
||||||
|
wsDisposables: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs = reactive<TerminalTab[]>([])
|
||||||
|
const activeTabId = ref('')
|
||||||
|
const wrapperRef = ref<HTMLDivElement | null>(null)
|
||||||
|
let tabCounter = 0
|
||||||
|
let resizeObserver: ResizeObserver | null = null
|
||||||
|
let fitTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let cacheTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
const CACHE_DEBOUNCE_MS = 2000
|
||||||
|
|
||||||
|
const activeTermTab = computed(() => tabs.find((t) => t.id === activeTabId.value))
|
||||||
|
|
||||||
|
function resolveTerminalWsUrl(botIdValue: string, cols: number, rows: number): string {
|
||||||
|
const baseUrl = String(client.getConfig().baseUrl || '').trim()
|
||||||
|
const token = localStorage.getItem('token') ?? ''
|
||||||
|
const path = `/bots/${encodeURIComponent(botIdValue)}/container/terminal/ws`
|
||||||
|
const query = `?token=${encodeURIComponent(token)}&cols=${cols}&rows=${rows}`
|
||||||
|
|
||||||
|
if (!baseUrl || baseUrl.startsWith('/')) {
|
||||||
|
const loc = window.location
|
||||||
|
const proto = loc.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
|
const base = baseUrl || '/api'
|
||||||
|
return `${proto}//${loc.host}${base.replace(/\/+$/, '')}${path}${query}`
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(path, baseUrl)
|
||||||
|
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
|
return url.toString() + query
|
||||||
|
} catch {
|
||||||
|
const loc = window.location
|
||||||
|
const proto = loc.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
|
return `${proto}//${loc.host}/api${path}${query}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTerminalTab(label?: string, cachedContent?: string): TerminalTab {
|
||||||
|
tabCounter++
|
||||||
|
const id = `term-${Date.now()}-${tabCounter}`
|
||||||
|
const tab = makeTab(id, label ?? `${t('bots.terminal.defaultTabLabel')} ${tabCounter}`)
|
||||||
|
tabs.push(tab)
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
const el = document.getElementById(id)
|
||||||
|
if (!el) return
|
||||||
|
tab.containerEl = el as HTMLDivElement
|
||||||
|
initTerminal(tab, cachedContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
return tab
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTerminal(tab: TerminalTab, cachedContent?: string) {
|
||||||
|
if (!tab.containerEl) return
|
||||||
|
|
||||||
|
const terminal = new Terminal({ ...TERMINAL_OPTIONS })
|
||||||
|
const fitAddon = new FitAddon()
|
||||||
|
const serializeAddon = new SerializeAddon()
|
||||||
|
terminal.loadAddon(fitAddon)
|
||||||
|
terminal.loadAddon(serializeAddon)
|
||||||
|
terminal.open(tab.containerEl)
|
||||||
|
|
||||||
|
tab.terminal = terminal
|
||||||
|
tab.fitAddon = fitAddon
|
||||||
|
tab.serializeAddon = serializeAddon
|
||||||
|
|
||||||
|
if (cachedContent) {
|
||||||
|
terminal.write(cachedContent)
|
||||||
|
terminal.write('\x1b[2K\r')
|
||||||
|
}
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
if (tab.id === activeTabId.value) fitAddon.fit()
|
||||||
|
connectWs(tab)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectWs(tab: TerminalTab) {
|
||||||
|
if (!tab.terminal) return
|
||||||
|
closeWs(tab)
|
||||||
|
|
||||||
|
if (tab.id === activeTabId.value) {
|
||||||
|
tab.fitAddon?.fit()
|
||||||
|
}
|
||||||
|
|
||||||
|
const cols = tab.terminal.cols
|
||||||
|
const rows = tab.terminal.rows
|
||||||
|
|
||||||
|
tab.status = 'connecting'
|
||||||
|
const url = resolveTerminalWsUrl(botId.value, cols, rows)
|
||||||
|
const ws = new WebSocket(url)
|
||||||
|
ws.binaryType = 'arraybuffer'
|
||||||
|
tab.ws = ws
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
tab.status = 'connected'
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
if (event.data instanceof ArrayBuffer) {
|
||||||
|
tab.terminal?.write(new Uint8Array(event.data))
|
||||||
|
} else if (typeof event.data === 'string') {
|
||||||
|
tab.terminal?.write(event.data)
|
||||||
|
}
|
||||||
|
debouncedPersistCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
tab.status = 'disconnected'
|
||||||
|
tab.terminal?.write('\r\n\x1b[31m[Connection closed]\x1b[0m\r\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onerror = () => {
|
||||||
|
tab.status = 'disconnected'
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const d of tab.wsDisposables) d.dispose()
|
||||||
|
tab.wsDisposables = []
|
||||||
|
|
||||||
|
tab.wsDisposables.push(
|
||||||
|
tab.terminal.onData((data) => {
|
||||||
|
if (tab.ws && tab.ws.readyState === WebSocket.OPEN) {
|
||||||
|
tab.ws.send(new TextEncoder().encode(data))
|
||||||
|
}
|
||||||
|
debouncedPersistCache()
|
||||||
|
}),
|
||||||
|
tab.terminal.onResize(({ cols: c, rows: r }) => {
|
||||||
|
if (tab.ws && tab.ws.readyState === WebSocket.OPEN) {
|
||||||
|
tab.ws.send(JSON.stringify({ type: 'resize', cols: c, rows: r }))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeWs(tab: TerminalTab) {
|
||||||
|
if (tab.ws) {
|
||||||
|
tab.ws.onclose = null
|
||||||
|
tab.ws.onerror = null
|
||||||
|
tab.ws.onmessage = null
|
||||||
|
tab.ws.close()
|
||||||
|
tab.ws = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyTab(tab: TerminalTab) {
|
||||||
|
closeWs(tab)
|
||||||
|
for (const d of tab.wsDisposables) d.dispose()
|
||||||
|
tab.wsDisposables = []
|
||||||
|
tab.terminal?.dispose()
|
||||||
|
tab.terminal = null
|
||||||
|
tab.fitAddon = null
|
||||||
|
tab.serializeAddon = null
|
||||||
|
tab.containerEl = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddTab() {
|
||||||
|
const tab = createTerminalTab()
|
||||||
|
activeTabId.value = tab.id
|
||||||
|
debouncedPersistCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCloseTab(tabId: string) {
|
||||||
|
const idx = tabs.findIndex((t) => t.id === tabId)
|
||||||
|
if (idx < 0) return
|
||||||
|
|
||||||
|
const target = tabs[idx]
|
||||||
|
if (target) destroyTab(target)
|
||||||
|
tabs.splice(idx, 1)
|
||||||
|
|
||||||
|
if (tabs.length === 0) {
|
||||||
|
handleAddTab()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTabId.value === tabId) {
|
||||||
|
const nextIdx = Math.min(idx, tabs.length - 1)
|
||||||
|
const next = tabs[nextIdx]
|
||||||
|
if (next) {
|
||||||
|
activeTabId.value = next.id
|
||||||
|
nextTick(() => next.fitAddon?.fit())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debouncedPersistCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSwitchTab(tabId: string) {
|
||||||
|
if (activeTabId.value === tabId) return
|
||||||
|
activeTabId.value = tabId
|
||||||
|
debouncedPersistCache()
|
||||||
|
nextTick(() => {
|
||||||
|
const tab = tabs.find((t) => t.id === tabId)
|
||||||
|
tab?.fitAddon?.fit()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReconnect() {
|
||||||
|
const tab = activeTermTab.value
|
||||||
|
if (tab) connectWs(tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistCache() {
|
||||||
|
if (!botId.value || tabs.length === 0) return
|
||||||
|
const state: TerminalCacheState = {
|
||||||
|
activeTabId: activeTabId.value,
|
||||||
|
tabs: tabs.map((tab) => ({
|
||||||
|
id: tab.id,
|
||||||
|
label: tab.label,
|
||||||
|
content: tab.serializeAddon?.serialize() ?? '',
|
||||||
|
savedAt: Date.now(),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
saveCache(botId.value, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
function debouncedPersistCache() {
|
||||||
|
if (cacheTimer) clearTimeout(cacheTimer)
|
||||||
|
cacheTimer = setTimeout(() => {
|
||||||
|
persistCache()
|
||||||
|
}, CACHE_DEBOUNCE_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreFromCache() {
|
||||||
|
const cached = loadCache(botId.value)
|
||||||
|
if (!cached || cached.tabs.length === 0) {
|
||||||
|
handleAddTab()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cachedTab of cached.tabs) {
|
||||||
|
tabCounter++
|
||||||
|
const tab = makeTab(cachedTab.id, cachedTab.label)
|
||||||
|
tabs.push(tab)
|
||||||
|
const content = cachedTab.content
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
const el = document.getElementById(tab.id)
|
||||||
|
if (!el) return
|
||||||
|
tab.containerEl = el as HTMLDivElement
|
||||||
|
initTerminal(tab, content)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstTab = cached.tabs[0]
|
||||||
|
const targetId = cached.tabs.find((ct) => ct.id === cached.activeTabId)
|
||||||
|
? cached.activeTabId
|
||||||
|
: firstTab?.id ?? ''
|
||||||
|
activeTabId.value = targetId
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBeforeUnload() {
|
||||||
|
persistCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupAll() {
|
||||||
|
if (fitTimer) {
|
||||||
|
clearTimeout(fitTimer)
|
||||||
|
fitTimer = null
|
||||||
|
}
|
||||||
|
if (cacheTimer) {
|
||||||
|
clearTimeout(cacheTimer)
|
||||||
|
cacheTimer = null
|
||||||
|
}
|
||||||
|
resizeObserver?.disconnect()
|
||||||
|
resizeObserver = null
|
||||||
|
for (const tab of tabs) {
|
||||||
|
destroyTab(tab)
|
||||||
|
}
|
||||||
|
tabs.length = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupResizeObserver() {
|
||||||
|
if (resizeObserver || !wrapperRef.value) return
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
if (fitTimer) clearTimeout(fitTimer)
|
||||||
|
fitTimer = setTimeout(() => {
|
||||||
|
activeTermTab.value?.fitAddon?.fit()
|
||||||
|
}, 50)
|
||||||
|
})
|
||||||
|
resizeObserver.observe(wrapperRef.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
window.addEventListener('beforeunload', onBeforeUnload)
|
||||||
|
restoreFromCache()
|
||||||
|
nextTick(() => setupResizeObserver())
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (activeTab.value === 'terminal') {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(activeTab, (tab) => {
|
||||||
|
if (tab === 'terminal' && tabs.length === 0) {
|
||||||
|
nextTick(() => init())
|
||||||
|
}
|
||||||
|
if (tab !== 'terminal' && tabs.length > 0) {
|
||||||
|
persistCache()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('beforeunload', onBeforeUnload)
|
||||||
|
persistCache()
|
||||||
|
cleanupAll()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col absolute inset-0 p-4">
|
||||||
|
<!-- Tab bar -->
|
||||||
|
<div class="flex items-center gap-1 mb-2 min-h-[36px]">
|
||||||
|
<div class="flex items-center gap-1 flex-1 overflow-x-auto">
|
||||||
|
<button
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.id"
|
||||||
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md border transition-colors whitespace-nowrap"
|
||||||
|
:class="tab.id === activeTabId
|
||||||
|
? 'bg-accent text-accent-foreground border-border'
|
||||||
|
: 'text-muted-foreground border-transparent hover:bg-accent/50'"
|
||||||
|
@click="handleSwitchTab(tab.id)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="size-1.5 rounded-full shrink-0"
|
||||||
|
:class="{
|
||||||
|
'bg-green-500': tab.status === 'connected',
|
||||||
|
'bg-yellow-500': tab.status === 'connecting',
|
||||||
|
'bg-muted-foreground': tab.status === 'idle' || tab.status === 'disconnected',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<span>{{ tab.label }}</span>
|
||||||
|
<button
|
||||||
|
class="ml-1 size-4 inline-flex items-center justify-center rounded hover:bg-destructive/20 hover:text-destructive"
|
||||||
|
:title="t('bots.terminal.closeTab')"
|
||||||
|
@click.stop="handleCloseTab(tab.id)"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center size-7 rounded-md border border-dashed border-border text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground transition-colors shrink-0"
|
||||||
|
:title="t('bots.terminal.newTab')"
|
||||||
|
@click="handleAddTab"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 shrink-0 ml-2">
|
||||||
|
<span
|
||||||
|
v-if="activeTermTab"
|
||||||
|
class="inline-flex items-center gap-1.5 text-xs"
|
||||||
|
:class="{
|
||||||
|
'text-green-500': activeTermTab.status === 'connected',
|
||||||
|
'text-yellow-500': activeTermTab.status === 'connecting',
|
||||||
|
'text-muted-foreground': activeTermTab.status === 'idle' || activeTermTab.status === 'disconnected',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ t(`bots.terminal.status.${activeTermTab.status}`) }}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
v-if="activeTermTab?.status === 'disconnected'"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
@click="handleReconnect"
|
||||||
|
>
|
||||||
|
{{ t('bots.terminal.reconnect') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Terminal area -->
|
||||||
|
<div
|
||||||
|
ref="wrapperRef"
|
||||||
|
class="flex-1 relative min-h-0 rounded-md overflow-hidden border border-border terminal-wrapper"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:id="tab.id"
|
||||||
|
:key="tab.id"
|
||||||
|
class="absolute inset-0 terminal-container"
|
||||||
|
:style="{ display: tab.id === activeTabId ? 'block' : 'none' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.terminal-wrapper {
|
||||||
|
background-color: #1a1b26;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-container {
|
||||||
|
top: 8px;
|
||||||
|
left: 8px;
|
||||||
|
right: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -248,6 +248,7 @@ import BotSubagents from './components/bot-subagents.vue'
|
|||||||
import BotOverview from './components/bot-overview.vue'
|
import BotOverview from './components/bot-overview.vue'
|
||||||
import BotSchedule from './components/bot-schedule.vue'
|
import BotSchedule from './components/bot-schedule.vue'
|
||||||
import BotContainer from './components/bot-container.vue'
|
import BotContainer from './components/bot-container.vue'
|
||||||
|
import BotTerminal from './components/bot-terminal.vue'
|
||||||
import BotFiles from './components/bot-files.vue'
|
import BotFiles from './components/bot-files.vue'
|
||||||
import { resolveApiErrorMessage } from '@/utils/api-error'
|
import { resolveApiErrorMessage } from '@/utils/api-error'
|
||||||
import { useAvatarInitials } from '@/composables/useAvatarInitials'
|
import { useAvatarInitials } from '@/composables/useAvatarInitials'
|
||||||
@@ -280,6 +281,7 @@ const tabList = computed(() => {
|
|||||||
{ value: 'channels', label: 'bots.tabs.channels', component: BotChannels, params: { 'bot-id': bot_id } },
|
{ value: 'channels', label: 'bots.tabs.channels', component: BotChannels, params: { 'bot-id': bot_id } },
|
||||||
{ value: 'email', label: 'bots.tabs.email', component: BotEmail, params: { 'bot-id': bot_id } },
|
{ value: 'email', label: 'bots.tabs.email', component: BotEmail, params: { 'bot-id': bot_id } },
|
||||||
{ value: 'container', label: 'bots.tabs.container', component: BotContainer, params: {} },
|
{ value: 'container', label: 'bots.tabs.container', component: BotContainer, params: {} },
|
||||||
|
{ value: 'terminal', label: 'bots.tabs.terminal', component: BotTerminal, params: { 'bot-id': bot_id } },
|
||||||
{ value: 'files', label: 'bots.tabs.files', component: BotFiles, params: { 'bot-id': bot_id, 'bot-type': bot.value?.type } },
|
{ value: 'files', label: 'bots.tabs.files', component: BotFiles, params: { 'bot-id': bot_id, 'bot-type': bot.value?.type } },
|
||||||
{ value: 'mcp', label: 'bots.tabs.mcp', component: BotMcp, params: { 'bot-id': bot_id } },
|
{ value: 'mcp', label: 'bots.tabs.mcp', component: BotMcp, params: { 'bot-id': bot_id } },
|
||||||
{ value: 'subagents', label: 'bots.tabs.subagents', component: BotSubagents, params: { 'bot-id': bot_id } },
|
{ value: 'subagents', label: 'bots.tabs.subagents', component: BotSubagents, params: { 'bot-id': bot_id } },
|
||||||
|
|||||||
+74
-6
@@ -16,6 +16,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/creack/pty"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
@@ -171,7 +172,6 @@ func (*containerServer) ListDir(_ context.Context, req *pb.ListDirRequest) (*pb.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (*containerServer) Exec(stream pb.ContainerService_ExecServer) error {
|
func (*containerServer) Exec(stream pb.ContainerService_ExecServer) error {
|
||||||
// Receive first message to get command details
|
|
||||||
firstMsg, err := stream.Recv()
|
firstMsg, err := stream.Recv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.Error(codes.InvalidArgument, "failed to receive exec config")
|
return status.Error(codes.InvalidArgument, "failed to receive exec config")
|
||||||
@@ -182,6 +182,77 @@ func (*containerServer) Exec(stream pb.ContainerService_ExecServer) error {
|
|||||||
return status.Error(codes.InvalidArgument, "command is required")
|
return status.Error(codes.InvalidArgument, "command is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if firstMsg.GetPty() {
|
||||||
|
return execPTY(stream, firstMsg)
|
||||||
|
}
|
||||||
|
return execPipe(stream, firstMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func execPTY(stream pb.ContainerService_ExecServer, firstMsg *pb.ExecInput) error {
|
||||||
|
command := firstMsg.GetCommand()
|
||||||
|
workDir := firstMsg.GetWorkDir()
|
||||||
|
if workDir == "" {
|
||||||
|
workDir = defaultWorkDir
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(stream.Context(), "/bin/sh", "-c", command) //nolint:gosec // G204: intentional
|
||||||
|
cmd.Dir = workDir
|
||||||
|
cmd.Env = append(os.Environ(), firstMsg.GetEnv()...)
|
||||||
|
cmd.Env = append(cmd.Env, "TERM=xterm-256color")
|
||||||
|
|
||||||
|
initialSize := &pty.Winsize{Rows: 24, Cols: 80}
|
||||||
|
if r := firstMsg.GetResize(); r != nil && r.GetCols() > 0 && r.GetRows() > 0 {
|
||||||
|
initialSize.Rows = uint16(r.GetRows()) //nolint:gosec // G115
|
||||||
|
initialSize.Cols = uint16(r.GetCols()) //nolint:gosec // G115
|
||||||
|
}
|
||||||
|
|
||||||
|
ptmx, err := pty.StartWithSize(cmd, initialSize)
|
||||||
|
if err != nil {
|
||||||
|
return status.Errorf(codes.Internal, "pty start: %v", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = ptmx.Close() }()
|
||||||
|
|
||||||
|
// stdin + resize from stream
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
msg, recvErr := stream.Recv()
|
||||||
|
if recvErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r := msg.GetResize(); r != nil && r.GetCols() > 0 && r.GetRows() > 0 {
|
||||||
|
_ = pty.Setsize(ptmx, &pty.Winsize{
|
||||||
|
Rows: uint16(r.GetRows()), //nolint:gosec // G115
|
||||||
|
Cols: uint16(r.GetCols()), //nolint:gosec // G115
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if data := msg.GetStdinData(); len(data) > 0 {
|
||||||
|
_, _ = ptmx.Write(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// PTY output -> stream (single fd merges stdout+stderr)
|
||||||
|
streamPipe(stream, ptmx, pb.ExecOutput_STDOUT)
|
||||||
|
|
||||||
|
exitCode := int32(0)
|
||||||
|
if waitErr := cmd.Wait(); waitErr != nil {
|
||||||
|
exitErr := &exec.ExitError{}
|
||||||
|
if errors.As(waitErr, &exitErr) {
|
||||||
|
ec := exitErr.ExitCode()
|
||||||
|
exitCode = int32(max(math.MinInt32, min(math.MaxInt32, ec))) //nolint:gosec // G115
|
||||||
|
} else {
|
||||||
|
exitCode = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stream.Send(&pb.ExecOutput{
|
||||||
|
Stream: pb.ExecOutput_EXIT,
|
||||||
|
ExitCode: exitCode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func execPipe(stream pb.ContainerService_ExecServer, firstMsg *pb.ExecInput) error {
|
||||||
|
command := firstMsg.GetCommand()
|
||||||
workDir := firstMsg.GetWorkDir()
|
workDir := firstMsg.GetWorkDir()
|
||||||
if workDir == "" {
|
if workDir == "" {
|
||||||
workDir = defaultWorkDir
|
workDir = defaultWorkDir
|
||||||
@@ -201,7 +272,6 @@ func (*containerServer) Exec(stream pb.ContainerService_ExecServer) error {
|
|||||||
cmd.Env = append(os.Environ(), firstMsg.GetEnv()...)
|
cmd.Env = append(os.Environ(), firstMsg.GetEnv()...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup stdin pipe for bidirectional streaming
|
|
||||||
stdinPipe, err := cmd.StdinPipe()
|
stdinPipe, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.Errorf(codes.Internal, "stdin pipe: %v", err)
|
return status.Errorf(codes.Internal, "stdin pipe: %v", err)
|
||||||
@@ -220,11 +290,10 @@ func (*containerServer) Exec(stream pb.ContainerService_ExecServer) error {
|
|||||||
return status.Errorf(codes.Internal, "start: %v", err)
|
return status.Errorf(codes.Internal, "start: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle stdin from stream
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
msg, err := stream.Recv()
|
msg, recvErr := stream.Recv()
|
||||||
if err != nil {
|
if recvErr != nil {
|
||||||
_ = stdinPipe.Close()
|
_ = stdinPipe.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -234,7 +303,6 @@ func (*containerServer) Exec(stream pb.ContainerService_ExecServer) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Stream stdout/stderr to client
|
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
defer close(done)
|
defer close(done)
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ require (
|
|||||||
github.com/containerd/ttrpc v1.2.7 // indirect
|
github.com/containerd/ttrpc v1.2.7 // indirect
|
||||||
github.com/containerd/typeurl/v2 v2.2.3 // indirect
|
github.com/containerd/typeurl/v2 v2.2.3 // indirect
|
||||||
github.com/containernetworking/cni v1.3.0 // indirect
|
github.com/containernetworking/cni v1.3.0 // indirect
|
||||||
|
github.com/creack/pty v1.1.24 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsx
|
|||||||
github.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEmnuFjskwo=
|
github.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEmnuFjskwo=
|
||||||
github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4=
|
github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
|||||||
@@ -142,6 +142,9 @@ func (h *ContainerdHandler) Register(e *echo.Echo) {
|
|||||||
group.GET("/skills", h.ListSkills)
|
group.GET("/skills", h.ListSkills)
|
||||||
group.POST("/skills", h.UpsertSkills)
|
group.POST("/skills", h.UpsertSkills)
|
||||||
group.DELETE("/skills", h.DeleteSkills)
|
group.DELETE("/skills", h.DeleteSkills)
|
||||||
|
// Terminal routes
|
||||||
|
group.GET("/terminal", h.GetTerminalInfo)
|
||||||
|
group.GET("/terminal/ws", h.HandleTerminalWS)
|
||||||
// File manager routes
|
// File manager routes
|
||||||
group.GET("/fs", h.FSStat)
|
group.GET("/fs", h.FSStat)
|
||||||
group.GET("/fs/list", h.FSList)
|
group.GET("/fs/list", h.FSList)
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
|
||||||
|
pb "github.com/memohai/memoh/internal/mcp/mcpcontainer"
|
||||||
|
)
|
||||||
|
|
||||||
|
var terminalUpgrader = websocket.Upgrader{
|
||||||
|
CheckOrigin: func(_ *http.Request) bool { return true },
|
||||||
|
}
|
||||||
|
|
||||||
|
type terminalInfoResponse struct {
|
||||||
|
Available bool `json:"available"`
|
||||||
|
Shell string `json:"shell"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type terminalControlMessage struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Cols uint32 `json:"cols,omitempty"`
|
||||||
|
Rows uint32 `json:"rows,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTerminalInfo godoc
|
||||||
|
// @Summary Check terminal availability for bot container
|
||||||
|
// @Tags containerd
|
||||||
|
// @Param bot_id path string true "Bot ID"
|
||||||
|
// @Success 200 {object} terminalInfoResponse
|
||||||
|
// @Failure 404 {object} ErrorResponse
|
||||||
|
// @Router /bots/{bot_id}/container/terminal [get].
|
||||||
|
func (h *ContainerdHandler) GetTerminalInfo(c echo.Context) error {
|
||||||
|
botID, err := h.requireBotAccess(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
|
||||||
|
if h.manager == nil {
|
||||||
|
return c.JSON(http.StatusOK, terminalInfoResponse{Available: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
client, clientErr := h.manager.MCPClient(ctx, botID)
|
||||||
|
if clientErr != nil || client == nil {
|
||||||
|
return c.JSON(http.StatusOK, terminalInfoResponse{Available: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, terminalInfoResponse{
|
||||||
|
Available: true,
|
||||||
|
Shell: "/bin/sh",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleTerminalWS godoc
|
||||||
|
// @Summary Interactive WebSocket terminal for bot container
|
||||||
|
// @Tags containerd
|
||||||
|
// @Param bot_id path string true "Bot ID"
|
||||||
|
// @Param cols query int false "Initial terminal columns" default(80)
|
||||||
|
// @Param rows query int false "Initial terminal rows" default(24)
|
||||||
|
// @Param token query string false "Auth token"
|
||||||
|
// @Success 101 "WebSocket upgrade"
|
||||||
|
// @Failure 400 {object} ErrorResponse
|
||||||
|
// @Failure 500 {object} ErrorResponse
|
||||||
|
// @Router /bots/{bot_id}/container/terminal/ws [get].
|
||||||
|
func (h *ContainerdHandler) HandleTerminalWS(c echo.Context) error {
|
||||||
|
botID, err := h.requireBotAccess(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
|
||||||
|
if h.manager == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "manager not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := h.manager.MCPClient(ctx, botID)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "container not reachable: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
cols := parseUint32Query(c, "cols", 80)
|
||||||
|
rows := parseUint32Query(c, "rows", 24)
|
||||||
|
|
||||||
|
conn, err := terminalUpgrader.Upgrade(c.Response(), c.Request(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
|
execStream, err := client.ExecStreamPTY(ctx, "/bin/sh", "/data", cols, rows)
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.WriteMessage(websocket.CloseMessage,
|
||||||
|
websocket.FormatCloseMessage(websocket.CloseInternalServerErr, "exec failed"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer func() { _ = execStream.Close() }()
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
// gRPC output -> WebSocket
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
for {
|
||||||
|
output, recvErr := execStream.Recv()
|
||||||
|
if recvErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch output.GetStream() {
|
||||||
|
case pb.ExecOutput_STDOUT, pb.ExecOutput_STDERR:
|
||||||
|
if data := output.GetData(); len(data) > 0 {
|
||||||
|
if writeErr := conn.WriteMessage(websocket.BinaryMessage, data); writeErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case pb.ExecOutput_EXIT:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// WebSocket -> gRPC stdin/resize
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
msgType, data, readErr := conn.ReadMessage()
|
||||||
|
if readErr != nil {
|
||||||
|
_ = execStream.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch msgType {
|
||||||
|
case websocket.BinaryMessage:
|
||||||
|
if len(data) > 0 {
|
||||||
|
if sendErr := execStream.SendStdin(data); sendErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case websocket.TextMessage:
|
||||||
|
var ctrl terminalControlMessage
|
||||||
|
if json.Unmarshal(data, &ctrl) == nil && ctrl.Type == "resize" && ctrl.Cols > 0 && ctrl.Rows > 0 {
|
||||||
|
if resizeErr := execStream.Resize(ctrl.Cols, ctrl.Rows); resizeErr != nil {
|
||||||
|
h.logger.Warn("terminal resize failed",
|
||||||
|
slog.String("bot_id", botID), slog.Any("error", resizeErr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-done
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUint32Query(c echo.Context, name string, fallback uint32) uint32 {
|
||||||
|
raw := c.QueryParam(name)
|
||||||
|
if raw == "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
v, err := strconv.ParseUint(raw, 10, 32)
|
||||||
|
if err != nil || v == 0 {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return uint32(v) //nolint:gosec // G115
|
||||||
|
}
|
||||||
@@ -204,11 +204,39 @@ func (s *ExecStream) Recv() (*pb.ExecOutput, error) {
|
|||||||
return s.stream.Recv()
|
return s.stream.Recv()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resize sends a terminal resize event to the running process.
|
||||||
|
func (s *ExecStream) Resize(cols, rows uint32) error {
|
||||||
|
return s.stream.Send(&pb.ExecInput{
|
||||||
|
Resize: &pb.TerminalResize{Cols: cols, Rows: rows},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Close closes the stream.
|
// Close closes the stream.
|
||||||
func (s *ExecStream) Close() error {
|
func (s *ExecStream) Close() error {
|
||||||
return s.stream.CloseSend()
|
return s.stream.CloseSend()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExecStreamPTY opens a bidirectional PTY exec stream.
|
||||||
|
// The command runs inside a pseudo-terminal with the given initial size.
|
||||||
|
func (c *Client) ExecStreamPTY(ctx context.Context, command, workDir string, cols, rows uint32) (*ExecStream, error) {
|
||||||
|
stream, err := c.svc.Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, mapError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = stream.Send(&pb.ExecInput{
|
||||||
|
Command: command,
|
||||||
|
WorkDir: workDir,
|
||||||
|
Pty: true,
|
||||||
|
Resize: &pb.TerminalResize{Cols: cols, Rows: rows},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ExecStream{stream: stream}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ReadRaw streams raw file bytes. Caller must consume the returned reader.
|
// ReadRaw streams raw file bytes. Caller must consume the returned reader.
|
||||||
func (c *Client) ReadRaw(ctx context.Context, path string) (io.ReadCloser, error) {
|
func (c *Client) ReadRaw(ctx context.Context, path string) (io.ReadCloser, error) {
|
||||||
stream, err := c.svc.ReadRaw(ctx, &pb.ReadRawRequest{Path: path})
|
stream, err := c.svc.ReadRaw(ctx, &pb.ReadRawRequest{Path: path})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.36.10
|
// protoc-gen-go v1.36.11
|
||||||
// protoc v4.25.3
|
// protoc v7.34.0
|
||||||
// source: internal/mcp/mcpcontainer/mcpcontainer.proto
|
// source: internal/mcp/mcpcontainer/mcpcontainer.proto
|
||||||
|
|
||||||
package mcpcontainer
|
package mcpcontainer
|
||||||
@@ -67,7 +67,7 @@ func (x ExecOutput_Stream) Number() protoreflect.EnumNumber {
|
|||||||
|
|
||||||
// Deprecated: Use ExecOutput_Stream.Descriptor instead.
|
// Deprecated: Use ExecOutput_Stream.Descriptor instead.
|
||||||
func (ExecOutput_Stream) EnumDescriptor() ([]byte, []int) {
|
func (ExecOutput_Stream) EnumDescriptor() ([]byte, []int) {
|
||||||
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{8, 0}
|
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{9, 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReadFileRequest struct {
|
type ReadFileRequest struct {
|
||||||
@@ -457,6 +457,8 @@ type ExecInput struct {
|
|||||||
Env []string `protobuf:"bytes,3,rep,name=env,proto3" json:"env,omitempty"`
|
Env []string `protobuf:"bytes,3,rep,name=env,proto3" json:"env,omitempty"`
|
||||||
TimeoutSeconds int32 `protobuf:"varint,4,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"`
|
TimeoutSeconds int32 `protobuf:"varint,4,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"`
|
||||||
StdinData []byte `protobuf:"bytes,5,opt,name=stdin_data,json=stdinData,proto3" json:"stdin_data,omitempty"`
|
StdinData []byte `protobuf:"bytes,5,opt,name=stdin_data,json=stdinData,proto3" json:"stdin_data,omitempty"`
|
||||||
|
Pty bool `protobuf:"varint,6,opt,name=pty,proto3" json:"pty,omitempty"`
|
||||||
|
Resize *TerminalResize `protobuf:"bytes,7,opt,name=resize,proto3" json:"resize,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@@ -526,6 +528,72 @@ func (x *ExecInput) GetStdinData() []byte {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *ExecInput) GetPty() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Pty
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExecInput) GetResize() *TerminalResize {
|
||||||
|
if x != nil {
|
||||||
|
return x.Resize
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type TerminalResize struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Cols uint32 `protobuf:"varint,1,opt,name=cols,proto3" json:"cols,omitempty"`
|
||||||
|
Rows uint32 `protobuf:"varint,2,opt,name=rows,proto3" json:"rows,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *TerminalResize) Reset() {
|
||||||
|
*x = TerminalResize{}
|
||||||
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[8]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *TerminalResize) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*TerminalResize) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *TerminalResize) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[8]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use TerminalResize.ProtoReflect.Descriptor instead.
|
||||||
|
func (*TerminalResize) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{8}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *TerminalResize) GetCols() uint32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Cols
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *TerminalResize) GetRows() uint32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Rows
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
type ExecOutput struct {
|
type ExecOutput struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Stream ExecOutput_Stream `protobuf:"varint,1,opt,name=stream,proto3,enum=mcpcontainer.ExecOutput_Stream" json:"stream,omitempty"`
|
Stream ExecOutput_Stream `protobuf:"varint,1,opt,name=stream,proto3,enum=mcpcontainer.ExecOutput_Stream" json:"stream,omitempty"`
|
||||||
@@ -537,7 +605,7 @@ type ExecOutput struct {
|
|||||||
|
|
||||||
func (x *ExecOutput) Reset() {
|
func (x *ExecOutput) Reset() {
|
||||||
*x = ExecOutput{}
|
*x = ExecOutput{}
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[8]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[9]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -549,7 +617,7 @@ func (x *ExecOutput) String() string {
|
|||||||
func (*ExecOutput) ProtoMessage() {}
|
func (*ExecOutput) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ExecOutput) ProtoReflect() protoreflect.Message {
|
func (x *ExecOutput) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[8]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[9]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -562,7 +630,7 @@ func (x *ExecOutput) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ExecOutput.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ExecOutput.ProtoReflect.Descriptor instead.
|
||||||
func (*ExecOutput) Descriptor() ([]byte, []int) {
|
func (*ExecOutput) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{8}
|
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{9}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ExecOutput) GetStream() ExecOutput_Stream {
|
func (x *ExecOutput) GetStream() ExecOutput_Stream {
|
||||||
@@ -595,7 +663,7 @@ type ReadRawRequest struct {
|
|||||||
|
|
||||||
func (x *ReadRawRequest) Reset() {
|
func (x *ReadRawRequest) Reset() {
|
||||||
*x = ReadRawRequest{}
|
*x = ReadRawRequest{}
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[9]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[10]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -607,7 +675,7 @@ func (x *ReadRawRequest) String() string {
|
|||||||
func (*ReadRawRequest) ProtoMessage() {}
|
func (*ReadRawRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ReadRawRequest) ProtoReflect() protoreflect.Message {
|
func (x *ReadRawRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[9]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[10]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -620,7 +688,7 @@ func (x *ReadRawRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ReadRawRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ReadRawRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*ReadRawRequest) Descriptor() ([]byte, []int) {
|
func (*ReadRawRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{9}
|
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{10}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ReadRawRequest) GetPath() string {
|
func (x *ReadRawRequest) GetPath() string {
|
||||||
@@ -639,7 +707,7 @@ type DataChunk struct {
|
|||||||
|
|
||||||
func (x *DataChunk) Reset() {
|
func (x *DataChunk) Reset() {
|
||||||
*x = DataChunk{}
|
*x = DataChunk{}
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[10]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[11]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -651,7 +719,7 @@ func (x *DataChunk) String() string {
|
|||||||
func (*DataChunk) ProtoMessage() {}
|
func (*DataChunk) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *DataChunk) ProtoReflect() protoreflect.Message {
|
func (x *DataChunk) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[10]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[11]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -664,7 +732,7 @@ func (x *DataChunk) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use DataChunk.ProtoReflect.Descriptor instead.
|
// Deprecated: Use DataChunk.ProtoReflect.Descriptor instead.
|
||||||
func (*DataChunk) Descriptor() ([]byte, []int) {
|
func (*DataChunk) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{10}
|
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{11}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *DataChunk) GetData() []byte {
|
func (x *DataChunk) GetData() []byte {
|
||||||
@@ -684,7 +752,7 @@ type WriteRawChunk struct {
|
|||||||
|
|
||||||
func (x *WriteRawChunk) Reset() {
|
func (x *WriteRawChunk) Reset() {
|
||||||
*x = WriteRawChunk{}
|
*x = WriteRawChunk{}
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[11]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[12]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -696,7 +764,7 @@ func (x *WriteRawChunk) String() string {
|
|||||||
func (*WriteRawChunk) ProtoMessage() {}
|
func (*WriteRawChunk) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *WriteRawChunk) ProtoReflect() protoreflect.Message {
|
func (x *WriteRawChunk) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[11]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[12]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -709,7 +777,7 @@ func (x *WriteRawChunk) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use WriteRawChunk.ProtoReflect.Descriptor instead.
|
// Deprecated: Use WriteRawChunk.ProtoReflect.Descriptor instead.
|
||||||
func (*WriteRawChunk) Descriptor() ([]byte, []int) {
|
func (*WriteRawChunk) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{11}
|
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{12}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *WriteRawChunk) GetPath() string {
|
func (x *WriteRawChunk) GetPath() string {
|
||||||
@@ -735,7 +803,7 @@ type WriteRawResponse struct {
|
|||||||
|
|
||||||
func (x *WriteRawResponse) Reset() {
|
func (x *WriteRawResponse) Reset() {
|
||||||
*x = WriteRawResponse{}
|
*x = WriteRawResponse{}
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[12]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[13]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -747,7 +815,7 @@ func (x *WriteRawResponse) String() string {
|
|||||||
func (*WriteRawResponse) ProtoMessage() {}
|
func (*WriteRawResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *WriteRawResponse) ProtoReflect() protoreflect.Message {
|
func (x *WriteRawResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[12]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[13]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -760,7 +828,7 @@ func (x *WriteRawResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use WriteRawResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use WriteRawResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*WriteRawResponse) Descriptor() ([]byte, []int) {
|
func (*WriteRawResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{12}
|
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{13}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *WriteRawResponse) GetBytesWritten() int64 {
|
func (x *WriteRawResponse) GetBytesWritten() int64 {
|
||||||
@@ -780,7 +848,7 @@ type DeleteFileRequest struct {
|
|||||||
|
|
||||||
func (x *DeleteFileRequest) Reset() {
|
func (x *DeleteFileRequest) Reset() {
|
||||||
*x = DeleteFileRequest{}
|
*x = DeleteFileRequest{}
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[13]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[14]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -792,7 +860,7 @@ func (x *DeleteFileRequest) String() string {
|
|||||||
func (*DeleteFileRequest) ProtoMessage() {}
|
func (*DeleteFileRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *DeleteFileRequest) ProtoReflect() protoreflect.Message {
|
func (x *DeleteFileRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[13]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[14]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -805,7 +873,7 @@ func (x *DeleteFileRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use DeleteFileRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use DeleteFileRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*DeleteFileRequest) Descriptor() ([]byte, []int) {
|
func (*DeleteFileRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{13}
|
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{14}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *DeleteFileRequest) GetPath() string {
|
func (x *DeleteFileRequest) GetPath() string {
|
||||||
@@ -830,7 +898,7 @@ type DeleteFileResponse struct {
|
|||||||
|
|
||||||
func (x *DeleteFileResponse) Reset() {
|
func (x *DeleteFileResponse) Reset() {
|
||||||
*x = DeleteFileResponse{}
|
*x = DeleteFileResponse{}
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[14]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[15]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -842,7 +910,7 @@ func (x *DeleteFileResponse) String() string {
|
|||||||
func (*DeleteFileResponse) ProtoMessage() {}
|
func (*DeleteFileResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *DeleteFileResponse) ProtoReflect() protoreflect.Message {
|
func (x *DeleteFileResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[14]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[15]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -855,7 +923,7 @@ func (x *DeleteFileResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use DeleteFileResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use DeleteFileResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*DeleteFileResponse) Descriptor() ([]byte, []int) {
|
func (*DeleteFileResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{14}
|
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{15}
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatRequest struct {
|
type StatRequest struct {
|
||||||
@@ -867,7 +935,7 @@ type StatRequest struct {
|
|||||||
|
|
||||||
func (x *StatRequest) Reset() {
|
func (x *StatRequest) Reset() {
|
||||||
*x = StatRequest{}
|
*x = StatRequest{}
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[15]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[16]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -879,7 +947,7 @@ func (x *StatRequest) String() string {
|
|||||||
func (*StatRequest) ProtoMessage() {}
|
func (*StatRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *StatRequest) ProtoReflect() protoreflect.Message {
|
func (x *StatRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[15]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[16]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -892,7 +960,7 @@ func (x *StatRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use StatRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use StatRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*StatRequest) Descriptor() ([]byte, []int) {
|
func (*StatRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{15}
|
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{16}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *StatRequest) GetPath() string {
|
func (x *StatRequest) GetPath() string {
|
||||||
@@ -911,7 +979,7 @@ type StatResponse struct {
|
|||||||
|
|
||||||
func (x *StatResponse) Reset() {
|
func (x *StatResponse) Reset() {
|
||||||
*x = StatResponse{}
|
*x = StatResponse{}
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[16]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[17]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -923,7 +991,7 @@ func (x *StatResponse) String() string {
|
|||||||
func (*StatResponse) ProtoMessage() {}
|
func (*StatResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *StatResponse) ProtoReflect() protoreflect.Message {
|
func (x *StatResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[16]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[17]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -936,7 +1004,7 @@ func (x *StatResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use StatResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use StatResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*StatResponse) Descriptor() ([]byte, []int) {
|
func (*StatResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{16}
|
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{17}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *StatResponse) GetEntry() *FileEntry {
|
func (x *StatResponse) GetEntry() *FileEntry {
|
||||||
@@ -955,7 +1023,7 @@ type MkdirRequest struct {
|
|||||||
|
|
||||||
func (x *MkdirRequest) Reset() {
|
func (x *MkdirRequest) Reset() {
|
||||||
*x = MkdirRequest{}
|
*x = MkdirRequest{}
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[17]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[18]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -967,7 +1035,7 @@ func (x *MkdirRequest) String() string {
|
|||||||
func (*MkdirRequest) ProtoMessage() {}
|
func (*MkdirRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *MkdirRequest) ProtoReflect() protoreflect.Message {
|
func (x *MkdirRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[17]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[18]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -980,7 +1048,7 @@ func (x *MkdirRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use MkdirRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use MkdirRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*MkdirRequest) Descriptor() ([]byte, []int) {
|
func (*MkdirRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{17}
|
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{18}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *MkdirRequest) GetPath() string {
|
func (x *MkdirRequest) GetPath() string {
|
||||||
@@ -998,7 +1066,7 @@ type MkdirResponse struct {
|
|||||||
|
|
||||||
func (x *MkdirResponse) Reset() {
|
func (x *MkdirResponse) Reset() {
|
||||||
*x = MkdirResponse{}
|
*x = MkdirResponse{}
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[18]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[19]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -1010,7 +1078,7 @@ func (x *MkdirResponse) String() string {
|
|||||||
func (*MkdirResponse) ProtoMessage() {}
|
func (*MkdirResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *MkdirResponse) ProtoReflect() protoreflect.Message {
|
func (x *MkdirResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[18]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[19]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -1023,7 +1091,7 @@ func (x *MkdirResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use MkdirResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use MkdirResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*MkdirResponse) Descriptor() ([]byte, []int) {
|
func (*MkdirResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{18}
|
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{19}
|
||||||
}
|
}
|
||||||
|
|
||||||
type RenameRequest struct {
|
type RenameRequest struct {
|
||||||
@@ -1036,7 +1104,7 @@ type RenameRequest struct {
|
|||||||
|
|
||||||
func (x *RenameRequest) Reset() {
|
func (x *RenameRequest) Reset() {
|
||||||
*x = RenameRequest{}
|
*x = RenameRequest{}
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[19]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[20]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -1048,7 +1116,7 @@ func (x *RenameRequest) String() string {
|
|||||||
func (*RenameRequest) ProtoMessage() {}
|
func (*RenameRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RenameRequest) ProtoReflect() protoreflect.Message {
|
func (x *RenameRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[19]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[20]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -1061,7 +1129,7 @@ func (x *RenameRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RenameRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RenameRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*RenameRequest) Descriptor() ([]byte, []int) {
|
func (*RenameRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{19}
|
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{20}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RenameRequest) GetOldPath() string {
|
func (x *RenameRequest) GetOldPath() string {
|
||||||
@@ -1086,7 +1154,7 @@ type RenameResponse struct {
|
|||||||
|
|
||||||
func (x *RenameResponse) Reset() {
|
func (x *RenameResponse) Reset() {
|
||||||
*x = RenameResponse{}
|
*x = RenameResponse{}
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[20]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[21]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -1098,7 +1166,7 @@ func (x *RenameResponse) String() string {
|
|||||||
func (*RenameResponse) ProtoMessage() {}
|
func (*RenameResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RenameResponse) ProtoReflect() protoreflect.Message {
|
func (x *RenameResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[20]
|
mi := &file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes[21]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -1111,7 +1179,7 @@ func (x *RenameResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RenameResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RenameResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*RenameResponse) Descriptor() ([]byte, []int) {
|
func (*RenameResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{20}
|
return file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP(), []int{21}
|
||||||
}
|
}
|
||||||
|
|
||||||
var File_internal_mcp_mcpcontainer_mcpcontainer_proto protoreflect.FileDescriptor
|
var File_internal_mcp_mcpcontainer_mcpcontainer_proto protoreflect.FileDescriptor
|
||||||
@@ -1143,14 +1211,19 @@ const file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDesc = "" +
|
|||||||
"\x04mode\x18\x04 \x01(\tR\x04mode\x12\x19\n" +
|
"\x04mode\x18\x04 \x01(\tR\x04mode\x12\x19\n" +
|
||||||
"\bmod_time\x18\x05 \x01(\tR\amodTime\"D\n" +
|
"\bmod_time\x18\x05 \x01(\tR\amodTime\"D\n" +
|
||||||
"\x0fListDirResponse\x121\n" +
|
"\x0fListDirResponse\x121\n" +
|
||||||
"\aentries\x18\x01 \x03(\v2\x17.mcpcontainer.FileEntryR\aentries\"\x9a\x01\n" +
|
"\aentries\x18\x01 \x03(\v2\x17.mcpcontainer.FileEntryR\aentries\"\xe2\x01\n" +
|
||||||
"\tExecInput\x12\x18\n" +
|
"\tExecInput\x12\x18\n" +
|
||||||
"\acommand\x18\x01 \x01(\tR\acommand\x12\x19\n" +
|
"\acommand\x18\x01 \x01(\tR\acommand\x12\x19\n" +
|
||||||
"\bwork_dir\x18\x02 \x01(\tR\aworkDir\x12\x10\n" +
|
"\bwork_dir\x18\x02 \x01(\tR\aworkDir\x12\x10\n" +
|
||||||
"\x03env\x18\x03 \x03(\tR\x03env\x12'\n" +
|
"\x03env\x18\x03 \x03(\tR\x03env\x12'\n" +
|
||||||
"\x0ftimeout_seconds\x18\x04 \x01(\x05R\x0etimeoutSeconds\x12\x1d\n" +
|
"\x0ftimeout_seconds\x18\x04 \x01(\x05R\x0etimeoutSeconds\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"stdin_data\x18\x05 \x01(\fR\tstdinData\"\xa2\x01\n" +
|
"stdin_data\x18\x05 \x01(\fR\tstdinData\x12\x10\n" +
|
||||||
|
"\x03pty\x18\x06 \x01(\bR\x03pty\x124\n" +
|
||||||
|
"\x06resize\x18\a \x01(\v2\x1c.mcpcontainer.TerminalResizeR\x06resize\"8\n" +
|
||||||
|
"\x0eTerminalResize\x12\x12\n" +
|
||||||
|
"\x04cols\x18\x01 \x01(\rR\x04cols\x12\x12\n" +
|
||||||
|
"\x04rows\x18\x02 \x01(\rR\x04rows\"\xa2\x01\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"ExecOutput\x127\n" +
|
"ExecOutput\x127\n" +
|
||||||
"\x06stream\x18\x01 \x01(\x0e2\x1f.mcpcontainer.ExecOutput.StreamR\x06stream\x12\x12\n" +
|
"\x06stream\x18\x01 \x01(\x0e2\x1f.mcpcontainer.ExecOutput.StreamR\x06stream\x12\x12\n" +
|
||||||
@@ -1212,7 +1285,7 @@ func file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDescGZIP() []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var file_internal_mcp_mcpcontainer_mcpcontainer_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
var file_internal_mcp_mcpcontainer_mcpcontainer_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||||
var file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes = make([]protoimpl.MessageInfo, 21)
|
var file_internal_mcp_mcpcontainer_mcpcontainer_proto_msgTypes = make([]protoimpl.MessageInfo, 22)
|
||||||
var file_internal_mcp_mcpcontainer_mcpcontainer_proto_goTypes = []any{
|
var file_internal_mcp_mcpcontainer_mcpcontainer_proto_goTypes = []any{
|
||||||
(ExecOutput_Stream)(0), // 0: mcpcontainer.ExecOutput.Stream
|
(ExecOutput_Stream)(0), // 0: mcpcontainer.ExecOutput.Stream
|
||||||
(*ReadFileRequest)(nil), // 1: mcpcontainer.ReadFileRequest
|
(*ReadFileRequest)(nil), // 1: mcpcontainer.ReadFileRequest
|
||||||
@@ -1223,49 +1296,51 @@ var file_internal_mcp_mcpcontainer_mcpcontainer_proto_goTypes = []any{
|
|||||||
(*FileEntry)(nil), // 6: mcpcontainer.FileEntry
|
(*FileEntry)(nil), // 6: mcpcontainer.FileEntry
|
||||||
(*ListDirResponse)(nil), // 7: mcpcontainer.ListDirResponse
|
(*ListDirResponse)(nil), // 7: mcpcontainer.ListDirResponse
|
||||||
(*ExecInput)(nil), // 8: mcpcontainer.ExecInput
|
(*ExecInput)(nil), // 8: mcpcontainer.ExecInput
|
||||||
(*ExecOutput)(nil), // 9: mcpcontainer.ExecOutput
|
(*TerminalResize)(nil), // 9: mcpcontainer.TerminalResize
|
||||||
(*ReadRawRequest)(nil), // 10: mcpcontainer.ReadRawRequest
|
(*ExecOutput)(nil), // 10: mcpcontainer.ExecOutput
|
||||||
(*DataChunk)(nil), // 11: mcpcontainer.DataChunk
|
(*ReadRawRequest)(nil), // 11: mcpcontainer.ReadRawRequest
|
||||||
(*WriteRawChunk)(nil), // 12: mcpcontainer.WriteRawChunk
|
(*DataChunk)(nil), // 12: mcpcontainer.DataChunk
|
||||||
(*WriteRawResponse)(nil), // 13: mcpcontainer.WriteRawResponse
|
(*WriteRawChunk)(nil), // 13: mcpcontainer.WriteRawChunk
|
||||||
(*DeleteFileRequest)(nil), // 14: mcpcontainer.DeleteFileRequest
|
(*WriteRawResponse)(nil), // 14: mcpcontainer.WriteRawResponse
|
||||||
(*DeleteFileResponse)(nil), // 15: mcpcontainer.DeleteFileResponse
|
(*DeleteFileRequest)(nil), // 15: mcpcontainer.DeleteFileRequest
|
||||||
(*StatRequest)(nil), // 16: mcpcontainer.StatRequest
|
(*DeleteFileResponse)(nil), // 16: mcpcontainer.DeleteFileResponse
|
||||||
(*StatResponse)(nil), // 17: mcpcontainer.StatResponse
|
(*StatRequest)(nil), // 17: mcpcontainer.StatRequest
|
||||||
(*MkdirRequest)(nil), // 18: mcpcontainer.MkdirRequest
|
(*StatResponse)(nil), // 18: mcpcontainer.StatResponse
|
||||||
(*MkdirResponse)(nil), // 19: mcpcontainer.MkdirResponse
|
(*MkdirRequest)(nil), // 19: mcpcontainer.MkdirRequest
|
||||||
(*RenameRequest)(nil), // 20: mcpcontainer.RenameRequest
|
(*MkdirResponse)(nil), // 20: mcpcontainer.MkdirResponse
|
||||||
(*RenameResponse)(nil), // 21: mcpcontainer.RenameResponse
|
(*RenameRequest)(nil), // 21: mcpcontainer.RenameRequest
|
||||||
|
(*RenameResponse)(nil), // 22: mcpcontainer.RenameResponse
|
||||||
}
|
}
|
||||||
var file_internal_mcp_mcpcontainer_mcpcontainer_proto_depIdxs = []int32{
|
var file_internal_mcp_mcpcontainer_mcpcontainer_proto_depIdxs = []int32{
|
||||||
6, // 0: mcpcontainer.ListDirResponse.entries:type_name -> mcpcontainer.FileEntry
|
6, // 0: mcpcontainer.ListDirResponse.entries:type_name -> mcpcontainer.FileEntry
|
||||||
0, // 1: mcpcontainer.ExecOutput.stream:type_name -> mcpcontainer.ExecOutput.Stream
|
9, // 1: mcpcontainer.ExecInput.resize:type_name -> mcpcontainer.TerminalResize
|
||||||
6, // 2: mcpcontainer.StatResponse.entry:type_name -> mcpcontainer.FileEntry
|
0, // 2: mcpcontainer.ExecOutput.stream:type_name -> mcpcontainer.ExecOutput.Stream
|
||||||
1, // 3: mcpcontainer.ContainerService.ReadFile:input_type -> mcpcontainer.ReadFileRequest
|
6, // 3: mcpcontainer.StatResponse.entry:type_name -> mcpcontainer.FileEntry
|
||||||
3, // 4: mcpcontainer.ContainerService.WriteFile:input_type -> mcpcontainer.WriteFileRequest
|
1, // 4: mcpcontainer.ContainerService.ReadFile:input_type -> mcpcontainer.ReadFileRequest
|
||||||
5, // 5: mcpcontainer.ContainerService.ListDir:input_type -> mcpcontainer.ListDirRequest
|
3, // 5: mcpcontainer.ContainerService.WriteFile:input_type -> mcpcontainer.WriteFileRequest
|
||||||
16, // 6: mcpcontainer.ContainerService.Stat:input_type -> mcpcontainer.StatRequest
|
5, // 6: mcpcontainer.ContainerService.ListDir:input_type -> mcpcontainer.ListDirRequest
|
||||||
18, // 7: mcpcontainer.ContainerService.Mkdir:input_type -> mcpcontainer.MkdirRequest
|
17, // 7: mcpcontainer.ContainerService.Stat:input_type -> mcpcontainer.StatRequest
|
||||||
20, // 8: mcpcontainer.ContainerService.Rename:input_type -> mcpcontainer.RenameRequest
|
19, // 8: mcpcontainer.ContainerService.Mkdir:input_type -> mcpcontainer.MkdirRequest
|
||||||
8, // 9: mcpcontainer.ContainerService.Exec:input_type -> mcpcontainer.ExecInput
|
21, // 9: mcpcontainer.ContainerService.Rename:input_type -> mcpcontainer.RenameRequest
|
||||||
10, // 10: mcpcontainer.ContainerService.ReadRaw:input_type -> mcpcontainer.ReadRawRequest
|
8, // 10: mcpcontainer.ContainerService.Exec:input_type -> mcpcontainer.ExecInput
|
||||||
12, // 11: mcpcontainer.ContainerService.WriteRaw:input_type -> mcpcontainer.WriteRawChunk
|
11, // 11: mcpcontainer.ContainerService.ReadRaw:input_type -> mcpcontainer.ReadRawRequest
|
||||||
14, // 12: mcpcontainer.ContainerService.DeleteFile:input_type -> mcpcontainer.DeleteFileRequest
|
13, // 12: mcpcontainer.ContainerService.WriteRaw:input_type -> mcpcontainer.WriteRawChunk
|
||||||
2, // 13: mcpcontainer.ContainerService.ReadFile:output_type -> mcpcontainer.ReadFileResponse
|
15, // 13: mcpcontainer.ContainerService.DeleteFile:input_type -> mcpcontainer.DeleteFileRequest
|
||||||
4, // 14: mcpcontainer.ContainerService.WriteFile:output_type -> mcpcontainer.WriteFileResponse
|
2, // 14: mcpcontainer.ContainerService.ReadFile:output_type -> mcpcontainer.ReadFileResponse
|
||||||
7, // 15: mcpcontainer.ContainerService.ListDir:output_type -> mcpcontainer.ListDirResponse
|
4, // 15: mcpcontainer.ContainerService.WriteFile:output_type -> mcpcontainer.WriteFileResponse
|
||||||
17, // 16: mcpcontainer.ContainerService.Stat:output_type -> mcpcontainer.StatResponse
|
7, // 16: mcpcontainer.ContainerService.ListDir:output_type -> mcpcontainer.ListDirResponse
|
||||||
19, // 17: mcpcontainer.ContainerService.Mkdir:output_type -> mcpcontainer.MkdirResponse
|
18, // 17: mcpcontainer.ContainerService.Stat:output_type -> mcpcontainer.StatResponse
|
||||||
21, // 18: mcpcontainer.ContainerService.Rename:output_type -> mcpcontainer.RenameResponse
|
20, // 18: mcpcontainer.ContainerService.Mkdir:output_type -> mcpcontainer.MkdirResponse
|
||||||
9, // 19: mcpcontainer.ContainerService.Exec:output_type -> mcpcontainer.ExecOutput
|
22, // 19: mcpcontainer.ContainerService.Rename:output_type -> mcpcontainer.RenameResponse
|
||||||
11, // 20: mcpcontainer.ContainerService.ReadRaw:output_type -> mcpcontainer.DataChunk
|
10, // 20: mcpcontainer.ContainerService.Exec:output_type -> mcpcontainer.ExecOutput
|
||||||
13, // 21: mcpcontainer.ContainerService.WriteRaw:output_type -> mcpcontainer.WriteRawResponse
|
12, // 21: mcpcontainer.ContainerService.ReadRaw:output_type -> mcpcontainer.DataChunk
|
||||||
15, // 22: mcpcontainer.ContainerService.DeleteFile:output_type -> mcpcontainer.DeleteFileResponse
|
14, // 22: mcpcontainer.ContainerService.WriteRaw:output_type -> mcpcontainer.WriteRawResponse
|
||||||
13, // [13:23] is the sub-list for method output_type
|
16, // 23: mcpcontainer.ContainerService.DeleteFile:output_type -> mcpcontainer.DeleteFileResponse
|
||||||
3, // [3:13] is the sub-list for method input_type
|
14, // [14:24] is the sub-list for method output_type
|
||||||
3, // [3:3] is the sub-list for extension type_name
|
4, // [4:14] is the sub-list for method input_type
|
||||||
3, // [3:3] is the sub-list for extension extendee
|
4, // [4:4] is the sub-list for extension type_name
|
||||||
0, // [0:3] is the sub-list for field type_name
|
4, // [4:4] is the sub-list for extension extendee
|
||||||
|
0, // [0:4] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_internal_mcp_mcpcontainer_mcpcontainer_proto_init() }
|
func init() { file_internal_mcp_mcpcontainer_mcpcontainer_proto_init() }
|
||||||
@@ -1279,7 +1354,7 @@ func file_internal_mcp_mcpcontainer_mcpcontainer_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDesc), len(file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDesc), len(file_internal_mcp_mcpcontainer_mcpcontainer_proto_rawDesc)),
|
||||||
NumEnums: 1,
|
NumEnums: 1,
|
||||||
NumMessages: 21,
|
NumMessages: 22,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -59,6 +59,13 @@ message ExecInput {
|
|||||||
repeated string env = 3;
|
repeated string env = 3;
|
||||||
int32 timeout_seconds = 4;
|
int32 timeout_seconds = 4;
|
||||||
bytes stdin_data = 5;
|
bytes stdin_data = 5;
|
||||||
|
bool pty = 6;
|
||||||
|
TerminalResize resize = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TerminalResize {
|
||||||
|
uint32 cols = 1;
|
||||||
|
uint32 rows = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ExecOutput {
|
message ExecOutput {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.5.1
|
// - protoc-gen-go-grpc v1.6.1
|
||||||
// - protoc v4.25.3
|
// - protoc v7.34.0
|
||||||
// source: internal/mcp/mcpcontainer/mcpcontainer.proto
|
// source: internal/mcp/mcpcontainer/mcpcontainer.proto
|
||||||
|
|
||||||
package mcpcontainer
|
package mcpcontainer
|
||||||
@@ -195,34 +195,34 @@ type ContainerServiceServer interface {
|
|||||||
type UnimplementedContainerServiceServer struct{}
|
type UnimplementedContainerServiceServer struct{}
|
||||||
|
|
||||||
func (UnimplementedContainerServiceServer) ReadFile(context.Context, *ReadFileRequest) (*ReadFileResponse, error) {
|
func (UnimplementedContainerServiceServer) ReadFile(context.Context, *ReadFileRequest) (*ReadFileResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method ReadFile not implemented")
|
return nil, status.Error(codes.Unimplemented, "method ReadFile not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedContainerServiceServer) WriteFile(context.Context, *WriteFileRequest) (*WriteFileResponse, error) {
|
func (UnimplementedContainerServiceServer) WriteFile(context.Context, *WriteFileRequest) (*WriteFileResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method WriteFile not implemented")
|
return nil, status.Error(codes.Unimplemented, "method WriteFile not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedContainerServiceServer) ListDir(context.Context, *ListDirRequest) (*ListDirResponse, error) {
|
func (UnimplementedContainerServiceServer) ListDir(context.Context, *ListDirRequest) (*ListDirResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method ListDir not implemented")
|
return nil, status.Error(codes.Unimplemented, "method ListDir not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedContainerServiceServer) Stat(context.Context, *StatRequest) (*StatResponse, error) {
|
func (UnimplementedContainerServiceServer) Stat(context.Context, *StatRequest) (*StatResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method Stat not implemented")
|
return nil, status.Error(codes.Unimplemented, "method Stat not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedContainerServiceServer) Mkdir(context.Context, *MkdirRequest) (*MkdirResponse, error) {
|
func (UnimplementedContainerServiceServer) Mkdir(context.Context, *MkdirRequest) (*MkdirResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method Mkdir not implemented")
|
return nil, status.Error(codes.Unimplemented, "method Mkdir not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedContainerServiceServer) Rename(context.Context, *RenameRequest) (*RenameResponse, error) {
|
func (UnimplementedContainerServiceServer) Rename(context.Context, *RenameRequest) (*RenameResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method Rename not implemented")
|
return nil, status.Error(codes.Unimplemented, "method Rename not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedContainerServiceServer) Exec(grpc.BidiStreamingServer[ExecInput, ExecOutput]) error {
|
func (UnimplementedContainerServiceServer) Exec(grpc.BidiStreamingServer[ExecInput, ExecOutput]) error {
|
||||||
return status.Errorf(codes.Unimplemented, "method Exec not implemented")
|
return status.Error(codes.Unimplemented, "method Exec not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedContainerServiceServer) ReadRaw(*ReadRawRequest, grpc.ServerStreamingServer[DataChunk]) error {
|
func (UnimplementedContainerServiceServer) ReadRaw(*ReadRawRequest, grpc.ServerStreamingServer[DataChunk]) error {
|
||||||
return status.Errorf(codes.Unimplemented, "method ReadRaw not implemented")
|
return status.Error(codes.Unimplemented, "method ReadRaw not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedContainerServiceServer) WriteRaw(grpc.ClientStreamingServer[WriteRawChunk, WriteRawResponse]) error {
|
func (UnimplementedContainerServiceServer) WriteRaw(grpc.ClientStreamingServer[WriteRawChunk, WriteRawResponse]) error {
|
||||||
return status.Errorf(codes.Unimplemented, "method WriteRaw not implemented")
|
return status.Error(codes.Unimplemented, "method WriteRaw not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedContainerServiceServer) DeleteFile(context.Context, *DeleteFileRequest) (*DeleteFileResponse, error) {
|
func (UnimplementedContainerServiceServer) DeleteFile(context.Context, *DeleteFileRequest) (*DeleteFileResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteFile not implemented")
|
return nil, status.Error(codes.Unimplemented, "method DeleteFile not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedContainerServiceServer) mustEmbedUnimplementedContainerServiceServer() {}
|
func (UnimplementedContainerServiceServer) mustEmbedUnimplementedContainerServiceServer() {}
|
||||||
func (UnimplementedContainerServiceServer) testEmbeddedByValue() {}
|
func (UnimplementedContainerServiceServer) testEmbeddedByValue() {}
|
||||||
@@ -235,7 +235,7 @@ type UnsafeContainerServiceServer interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RegisterContainerServiceServer(s grpc.ServiceRegistrar, srv ContainerServiceServer) {
|
func RegisterContainerServiceServer(s grpc.ServiceRegistrar, srv ContainerServiceServer) {
|
||||||
// If the following call pancis, it indicates UnimplementedContainerServiceServer was
|
// If the following call panics, it indicates UnimplementedContainerServiceServer was
|
||||||
// embedded by pointer and is nil. This will cause panics if an
|
// embedded by pointer and is nil. This will cause panics if an
|
||||||
// unimplemented method is ever invoked, so we test this at initialization
|
// unimplemented method is ever invoked, so we test this at initialization
|
||||||
// time to prevent it from happening at runtime later due to I/O.
|
// time to prevent it from happening at runtime later due to I/O.
|
||||||
|
|||||||
Generated
+24
@@ -158,6 +158,15 @@ importers:
|
|||||||
'@vueuse/core':
|
'@vueuse/core':
|
||||||
specifier: ^14.1.0
|
specifier: ^14.1.0
|
||||||
version: 14.1.0(vue@3.5.26(typescript@5.9.3))
|
version: 14.1.0(vue@3.5.26(typescript@5.9.3))
|
||||||
|
'@xterm/addon-fit':
|
||||||
|
specifier: ^0.11.0
|
||||||
|
version: 0.11.0
|
||||||
|
'@xterm/addon-serialize':
|
||||||
|
specifier: ^0.14.0
|
||||||
|
version: 0.14.0
|
||||||
|
'@xterm/xterm':
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: 6.0.0
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^17.2.3
|
specifier: ^17.2.3
|
||||||
version: 17.2.3
|
version: 17.2.3
|
||||||
@@ -2562,6 +2571,15 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.5.0
|
vue: ^3.5.0
|
||||||
|
|
||||||
|
'@xterm/addon-fit@0.11.0':
|
||||||
|
resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==}
|
||||||
|
|
||||||
|
'@xterm/addon-serialize@0.14.0':
|
||||||
|
resolution: {integrity: sha512-uteyTU1EkrQa2Ux6P/uFl2fzmXI46jy5uoQMKEOM0fKTyiW7cSn0WrFenHm5vO5uEXX/GpwW/FgILvv3r0WbkA==}
|
||||||
|
|
||||||
|
'@xterm/xterm@6.0.0':
|
||||||
|
resolution: {integrity: sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==}
|
||||||
|
|
||||||
accepts@2.0.0:
|
accepts@2.0.0:
|
||||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -7426,6 +7444,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.26(typescript@5.9.3)
|
vue: 3.5.26(typescript@5.9.3)
|
||||||
|
|
||||||
|
'@xterm/addon-fit@0.11.0': {}
|
||||||
|
|
||||||
|
'@xterm/addon-serialize@0.14.0': {}
|
||||||
|
|
||||||
|
'@xterm/xterm@6.0.0': {}
|
||||||
|
|
||||||
accepts@2.0.0:
|
accepts@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-types: 3.0.2
|
mime-types: 3.0.2
|
||||||
|
|||||||
Reference in New Issue
Block a user