Function bodies 92 total
installLaunchdService function · go · L22-L66 (45 LOC)service.go
func installLaunchdService(home string) error {
plistDir := filepath.Join(home, "Library", "LaunchAgents")
if err := os.MkdirAll(plistDir, 0755); err != nil {
return fmt.Errorf("failed to create LaunchAgents dir: %w", err)
}
plistPath := filepath.Join(plistDir, "com.ccc.plist")
logPath := filepath.Join(home, ".ccc.log")
plist := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.ccc</string>
<key>ProgramArguments</key>
<array>
<string>%s</string>
<string>listen</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>%s</string>
<key>StandardErrorPath</key>
<string>%s</string>
</dict>
</plist>
`, cccPath, logPath, logPath)
if err := os.WriteFile(plistPath, []byte(plist), 0644); err != nil {
reinstallSystemdService function · go · L68-L101 (34 LOC)service.go
func installSystemdService(home string) error {
serviceDir := filepath.Join(home, ".config", "systemd", "user")
if err := os.MkdirAll(serviceDir, 0755); err != nil {
return fmt.Errorf("failed to create systemd dir: %w", err)
}
servicePath := filepath.Join(serviceDir, "ccc.service")
service := fmt.Sprintf(`[Unit]
Description=Claude Code Companion
After=network.target
[Service]
ExecStart=%s listen
Restart=always
RestartSec=10
[Install]
WantedBy=default.target
`, cccPath)
if err := os.WriteFile(servicePath, []byte(service), 0644); err != nil {
return fmt.Errorf("failed to write service file: %w", err)
}
// Reload and start
exec.Command("systemctl", "--user", "daemon-reload").Run()
exec.Command("systemctl", "--user", "enable", "ccc").Run()
if err := exec.Command("systemctl", "--user", "start", "ccc").Run(); err != nil {
return fmt.Errorf("failed to start service: %w", err)
}
fmt.Println("✅ Service installed and started (systemd)")
return nil
}sessionName function · go · L12-L16 (5 LOC)session.go
func sessionName(name string) string {
// Replace dots with underscores - tmux interprets dots as window/pane separators
safeName := strings.ReplaceAll(name, ".", "_")
return "claude-" + safeName
}createSession function · go · L18-L51 (34 LOC)session.go
func createSession(config *Config, name string) error {
// Check if session already exists
if _, exists := config.Sessions[name]; exists {
return fmt.Errorf("session '%s' already exists", name)
}
// Create Telegram topic
topicID, err := createForumTopic(config, name)
if err != nil {
return fmt.Errorf("failed to create topic: %w", err)
}
// Create tmux session
workDir := resolveProjectPath(config, name)
if _, err := os.Stat(workDir); os.IsNotExist(err) {
// Create project directory
os.MkdirAll(workDir, 0755)
}
if err := createTmuxSession(sessionName(name), workDir, false); err != nil {
return fmt.Errorf("failed to create tmux session: %w", err)
}
// Save mapping with full path
config.Sessions[name] = &SessionInfo{
TopicID: topicID,
Path: workDir,
}
if err := saveConfig(config); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
return nil
}killSession function · go · L53-L66 (14 LOC)session.go
func killSession(config *Config, name string) error {
if _, exists := config.Sessions[name]; !exists {
return fmt.Errorf("session '%s' not found", name)
}
// Kill tmux session
killTmuxSession(sessionName(name))
// Remove from config
delete(config.Sessions, name)
saveConfig(config)
return nil
}getSessionByTopic function · go · L68-L75 (8 LOC)session.go
func getSessionByTopic(config *Config, topicID int64) string {
for name, info := range config.Sessions {
if info != nil && info.TopicID == topicID {
return name
}
}
return ""
}startSession function · go · L78-L146 (69 LOC)session.go
func startSession(continueSession bool) error {
// Get current directory name as session name
cwd, err := os.Getwd()
if err != nil {
return err
}
name := filepath.Base(cwd)
tmuxName := sessionName(name)
// Load config to check/create topic
config, err := loadConfig()
if err != nil {
// No config, just run claude directly
return runClaudeRaw(continueSession)
}
// Create topic if it doesn't exist and we have a group configured
if config.GroupID != 0 {
if _, exists := config.Sessions[name]; !exists {
topicID, err := createForumTopic(config, name)
if err == nil {
config.Sessions[name] = &SessionInfo{
TopicID: topicID,
Path: cwd,
}
saveConfig(config)
fmt.Printf("📱 Created Telegram topic: %s\n", name)
}
}
}
// Check if tmux session exists
if tmuxSessionExists(tmuxName) {
// Check if we're already inside tmux
if os.Getenv("TMUX") != "" {
// Inside tmux: switch to the session
cmd := exec.Command(tmuxPath, "switch-clienRepobility — the code-quality scanner for AI-generated software · https://repobility.com
startDetached function · go · L149-L198 (50 LOC)session.go
func startDetached(name string, workDir string, prompt string) error {
config, err := loadConfig()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
if config.Sessions == nil {
config.Sessions = make(map[string]*SessionInfo)
}
// Create Telegram topic
topicID, err := createForumTopic(config, name)
if err != nil {
return fmt.Errorf("failed to create topic: %w", err)
}
tmuxName := sessionName(name)
// Kill existing tmux session if any
if tmuxSessionExists(tmuxName) {
killTmuxSession(tmuxName)
}
// Create tmux session (detached)
if err := createTmuxSession(tmuxName, workDir, false); err != nil {
return fmt.Errorf("failed to create tmux session: %w", err)
}
// Save session info
config.Sessions[name] = &SessionInfo{
TopicID: topicID,
Path: workDir,
}
if err := saveConfig(config); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
// Wait for Claude to be ready before sending prompt
if err := waitForClaude(tredactTokenError function · go · L22-L27 (6 LOC)telegram.go
func redactTokenError(err error, token string) error {
if err == nil || token == "" {
return err
}
return fmt.Errorf("%s", strings.ReplaceAll(err.Error(), token, "***"))
}telegramGet function · go · L30-L36 (7 LOC)telegram.go
func telegramGet(token string, url string) (*http.Response, error) {
resp, err := http.Get(url)
if err != nil {
return nil, redactTokenError(err, token)
}
return resp, nil
}telegramClientGet function · go · L39-L45 (7 LOC)telegram.go
func telegramClientGet(client *http.Client, token string, url string) (*http.Response, error) {
resp, err := client.Get(url)
if err != nil {
return nil, redactTokenError(err, token)
}
return resp, nil
}updateCCC function · go · L48-L137 (90 LOC)telegram.go
func updateCCC(config *Config, chatID, threadID int64, offset int) {
sendMessage(config, chatID, threadID, "🔄 Updating ccc...")
binaryName := fmt.Sprintf("ccc-%s-%s", runtime.GOOS, runtime.GOARCH)
downloadURL := fmt.Sprintf("https://github.com/kidandcat/ccc/releases/latest/download/%s", binaryName)
resp, err := http.Get(downloadURL)
if err != nil {
sendMessage(config, chatID, threadID, fmt.Sprintf("❌ Download failed: %v", err))
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
sendMessage(config, chatID, threadID, fmt.Sprintf("❌ Download failed: HTTP %d (no release for %s?)", resp.StatusCode, binaryName))
return
}
tmpPath := cccPath + ".new"
f, err := os.Create(tmpPath)
if err != nil {
sendMessage(config, chatID, threadID, fmt.Sprintf("❌ Failed to create temp file: %v", err))
return
}
written, err := io.Copy(f, resp.Body)
f.Close()
if err != nil {
os.Remove(tmpPath)
sendMessage(config, chatID, threadID, fmt.Sprintf("❌ Failed to write binatelegramAPI function · go · L139-L378 (240 LOC)telegram.go
func telegramAPI(config *Config, method string, params url.Values) (*TelegramResponse, error) {
apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/%s", config.BotToken, method)
resp, err := http.PostForm(apiURL, params)
if err != nil {
return nil, redactTokenError(err, config.BotToken)
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, maxResponseSize))
var result TelegramResponse
json.Unmarshal(body, &result)
return &result, nil
}
func sendMessage(config *Config, chatID int64, threadID int64, text string) error {
_, err := sendMessageGetID(config, chatID, threadID, text)
return err
}
// sendMessageGetID sends a message and returns the message ID for later editing
func sendMessageGetID(config *Config, chatID int64, threadID int64, text string) (int64, error) {
const maxLen = 4000
// Split long messages
messages := splitMessage(text, maxLen)
var lastMsgID int64
for _, msg := range messages {
params := url.Values{
"chat_id": {fmt.SprisendMessage function · go · L153-L156 (4 LOC)telegram.go
func sendMessage(config *Config, chatID int64, threadID int64, text string) error {
_, err := sendMessageGetID(config, chatID, threadID, text)
return err
}sendMessageGetID function · go · L159-L199 (41 LOC)telegram.go
func sendMessageGetID(config *Config, chatID int64, threadID int64, text string) (int64, error) {
const maxLen = 4000
// Split long messages
messages := splitMessage(text, maxLen)
var lastMsgID int64
for _, msg := range messages {
params := url.Values{
"chat_id": {fmt.Sprintf("%d", chatID)},
"text": {msg},
}
if threadID > 0 {
params.Set("message_thread_id", fmt.Sprintf("%d", threadID))
}
result, err := telegramAPI(config, "sendMessage", params)
if err != nil {
return 0, err
}
if !result.OK {
return 0, fmt.Errorf("telegram error: %s", result.Description)
}
// Extract message_id from result
if len(result.Result) > 0 {
var msgResult struct {
MessageID int64 `json:"message_id"`
}
if json.Unmarshal(result.Result, &msgResult) == nil {
lastMsgID = msgResult.MessageID
}
}
// Small delay between messages to maintain order
if len(messages) > 1 {
time.Sleep(100 * time.Millisecond)
}
}
return lastMsgID, nil
}All rows above produced by Repobility · https://repobility.com
editMessage function · go · L202-L231 (30 LOC)telegram.go
func editMessage(config *Config, chatID int64, messageID int64, threadID int64, text string) error {
const maxLen = 4000
// Split message - first part goes to edit, rest as new messages
messages := splitMessage(text, maxLen)
// Edit existing message with first part
params := url.Values{
"chat_id": {fmt.Sprintf("%d", chatID)},
"message_id": {fmt.Sprintf("%d", messageID)},
"text": {messages[0]},
}
result, err := telegramAPI(config, "editMessageText", params)
if err != nil {
return err
}
if !result.OK {
// If edit fails (e.g., message not modified), ignore
return nil
}
// Send remaining parts as new messages
for i := 1; i < len(messages); i++ {
time.Sleep(100 * time.Millisecond)
sendMessage(config, chatID, threadID, messages[i])
}
return nil
}sendMessageWithKeyboard function · go · L233-L268 (36 LOC)telegram.go
func sendMessageWithKeyboard(config *Config, chatID int64, threadID int64, text string, buttons [][]InlineKeyboardButton) error {
const maxLen = 4000
// Split long messages - send all but last as regular messages, last with keyboard
messages := splitMessage(text, maxLen)
// Send all but the last message as regular messages
for i := 0; i < len(messages)-1; i++ {
sendMessage(config, chatID, threadID, messages[i])
time.Sleep(100 * time.Millisecond)
}
// Send the last message with keyboard
keyboard := map[string]interface{}{
"inline_keyboard": buttons,
}
keyboardJSON, _ := json.Marshal(keyboard)
params := url.Values{
"chat_id": {fmt.Sprintf("%d", chatID)},
"text": {messages[len(messages)-1]},
"reply_markup": {string(keyboardJSON)},
}
if threadID > 0 {
params.Set("message_thread_id", fmt.Sprintf("%d", threadID))
}
result, err := telegramAPI(config, "sendMessage", params)
if err != nil {
return err
}
if !result.OK {
return fmt.Errorf("telanswerCallbackQuery function · go · L270-L275 (6 LOC)telegram.go
func answerCallbackQuery(config *Config, callbackID string) {
params := url.Values{
"callback_query_id": {callbackID},
}
telegramAPI(config, "answerCallbackQuery", params)
}editMessageRemoveKeyboard function · go · L277-L289 (13 LOC)telegram.go
func editMessageRemoveKeyboard(config *Config, chatID int64, messageID int, newText string) {
const maxLen = 4000
if len(newText) > maxLen {
newText = newText[:maxLen-3] + "..."
}
params := url.Values{
"chat_id": {fmt.Sprintf("%d", chatID)},
"message_id": {fmt.Sprintf("%d", messageID)},
"text": {newText},
}
telegramAPI(config, "editMessageText", params)
}sendTypingAction function · go · L291-L300 (10 LOC)telegram.go
func sendTypingAction(config *Config, chatID int64, threadID int64) {
params := url.Values{
"chat_id": {fmt.Sprintf("%d", chatID)},
"action": {"typing"},
}
if threadID > 0 {
params.Set("message_thread_id", fmt.Sprintf("%d", threadID))
}
telegramAPI(config, "sendChatAction", params)
}splitMessage function · go · L302-L332 (31 LOC)telegram.go
func splitMessage(text string, maxLen int) []string {
if len(text) <= maxLen {
return []string{text}
}
var messages []string
remaining := text
for len(remaining) > 0 {
if len(remaining) <= maxLen {
messages = append(messages, remaining)
break
}
// Find a good split point (newline or space)
splitAt := maxLen
// Try to split at a newline first
if idx := strings.LastIndex(remaining[:maxLen], "\n"); idx > maxLen/2 {
splitAt = idx + 1
} else if idx := strings.LastIndex(remaining[:maxLen], " "); idx > maxLen/2 {
// Fall back to space
splitAt = idx + 1
}
messages = append(messages, strings.TrimRight(remaining[:splitAt], " \n"))
remaining = remaining[splitAt:]
}
return messages
}downloadTelegramFile function · go · L381-L418 (38 LOC)telegram.go
func downloadTelegramFile(config *Config, fileID string, destPath string) error {
// Get file path from Telegram
resp, err := telegramGet(config.BotToken, fmt.Sprintf("https://api.telegram.org/bot%s/getFile?file_id=%s", config.BotToken, fileID))
if err != nil {
return err
}
defer resp.Body.Close()
var result struct {
OK bool `json:"ok"`
Result struct {
FilePath string `json:"file_path"`
} `json:"result"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return err
}
if !result.OK {
return fmt.Errorf("failed to get file path")
}
// Download the file
fileURL := fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", config.BotToken, result.Result.FilePath)
fileResp, err := telegramGet(config.BotToken, fileURL)
if err != nil {
return err
}
defer fileResp.Body.Close()
out, err := os.Create(destPath)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, fileResp.Body)
return err
}createForumTopic function · go · L420-L444 (25 LOC)telegram.go
func createForumTopic(config *Config, name string) (int64, error) {
if config.GroupID == 0 {
return 0, fmt.Errorf("no group configured. Add bot to a group with topics enabled and run: ccc setgroup")
}
params := url.Values{
"chat_id": {fmt.Sprintf("%d", config.GroupID)},
"name": {name},
}
result, err := telegramAPI(config, "createForumTopic", params)
if err != nil {
return 0, err
}
if !result.OK {
return 0, fmt.Errorf("failed to create topic: %s", result.Description)
}
var topic TopicResult
if err := json.Unmarshal(result.Result, &topic); err != nil {
return 0, fmt.Errorf("failed to parse topic result: %w", err)
}
return topic.MessageThreadID, nil
}Methodology: Repobility · https://repobility.com/research/state-of-ai-code-2026/
deleteForumTopic function · go · L446-L465 (20 LOC)telegram.go
func deleteForumTopic(config *Config, topicID int64) error {
if config.GroupID == 0 {
return fmt.Errorf("no group configured")
}
params := url.Values{
"chat_id": {fmt.Sprintf("%d", config.GroupID)},
"message_thread_id": {fmt.Sprintf("%d", topicID)},
}
result, err := telegramAPI(config, "deleteForumTopic", params)
if err != nil {
return err
}
if !result.OK {
return fmt.Errorf("failed to delete topic: %s", result.Description)
}
return nil
}setBotCommands function · go · L468-L507 (40 LOC)telegram.go
func setBotCommands(botToken string) {
commands := []map[string]string{
{"command": "new", "description": "Create/restart session: /new <name>"},
{"command": "delete", "description": "Delete current session and thread"},
{"command": "cleanup", "description": "Delete ALL sessions, folders and threads"},
{"command": "c", "description": "Execute shell command: /c <cmd>"},
{"command": "continue", "description": "Restart session with history"},
{"command": "update", "description": "Update ccc binary from GitHub"},
{"command": "version", "description": "Show ccc version"},
{"command": "stats", "description": "Show system stats (RAM, disk, etc)"},
{"command": "auth", "description": "Re-authenticate Claude OAuth"},
}
// Set for default scope
defaultBody, _ := json.Marshal(map[string]interface{}{
"commands": commands,
})
resp, err := http.Post(
fmt.Sprintf("https://api.telegram.org/bot%s/setMyCommands", botToken),
"application/json",
bytes.NewReader(defaultBody)initPaths function · go · L19-L61 (43 LOC)tmux.go
func initPaths() {
// Find tmux binary
if path, err := exec.LookPath("tmux"); err == nil {
tmuxPath = path
} else {
// Fallback paths for common installations
for _, p := range []string{"/opt/homebrew/bin/tmux", "/usr/local/bin/tmux", "/usr/bin/tmux"} {
if _, err := os.Stat(p); err == nil {
tmuxPath = p
break
}
}
}
// Find ccc binary - prefer ~/bin/ccc (canonical install path),
// then PATH, then current executable as last resort
home, _ := os.UserHomeDir()
binCcc := home + "/bin/ccc"
if _, err := os.Stat(binCcc); err == nil {
cccPath = binCcc
} else if path, err := exec.LookPath("ccc"); err == nil {
cccPath = path
} else if exe, err := os.Executable(); err == nil {
cccPath = exe
}
// Find claude binary - first try PATH, then fallback paths
if path, err := exec.LookPath("claude"); err == nil {
claudePath = path
} else {
home, _ := os.UserHomeDir()
claudePaths := []string{
home + "/.local/bin/claude",
"/usr/local/bin/claude",
}
tmuxSessionExists function · go · L63-L66 (4 LOC)tmux.go
func tmuxSessionExists(name string) bool {
cmd := exec.Command(tmuxPath, "has-session", "-t", name)
return cmd.Run() == nil
}createTmuxSession function · go · L68-L90 (23 LOC)tmux.go
func createTmuxSession(name string, workDir string, continueSession bool) error {
// Build the command to run inside tmux
cccCmd := cccPath + " run"
if continueSession {
cccCmd += " -c"
}
// Create tmux session with a login shell (don't run command directly - it kills session on exit)
args := []string{"new-session", "-d", "-s", name, "-c", workDir}
cmd := exec.Command(tmuxPath, args...)
if err := cmd.Run(); err != nil {
return err
}
// Enable mouse mode for this session (allows scrolling)
exec.Command(tmuxPath, "set-option", "-t", name, "mouse", "on").Run()
// Send the command to the session via send-keys (preserves TTY properly)
time.Sleep(200 * time.Millisecond)
exec.Command(tmuxPath, "send-keys", "-t", name, cccCmd, "C-m").Run()
return nil
}runClaudeRaw function · go · L93-L126 (34 LOC)tmux.go
func runClaudeRaw(continueSession bool) error {
if claudePath == "" {
return fmt.Errorf("claude binary not found")
}
// Clean stale Telegram flag from previous sessions.
// This ensures a locally started session doesn't inherit a flag
// left behind when a previous session's stop hook didn't fire.
if tmuxName, err := exec.Command(tmuxPath, "display-message", "-p", "#{session_name}").Output(); err == nil {
name := strings.TrimSpace(string(tmuxName))
if name != "" {
os.Remove(telegramActiveFlag(name))
}
}
var args []string
if continueSession {
args = append(args, "-c")
}
cmd := exec.Command(claudePath, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// Ensure OAuth token is available from config if not already in environment
if os.Getenv("CLAUDE_CODE_OAUTH_TOKEN") == "" {
if config, err := loadConfig(); err == nil && config.OAuthToken != "" {
cmd.Env = append(os.Environ(), "CLAUDE_CODE_OAUTH_TOKEN="+config.OAuthToken)
}
waitForClaude function · go · L129-L144 (16 LOC)tmux.go
func waitForClaude(session string, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
cmd := exec.Command(tmuxPath, "capture-pane", "-t", session, "-p")
out, err := cmd.Output()
if err == nil {
content := string(out)
// Claude Code shows "❯" when ready for input
if strings.Contains(content, "❯") {
return nil
}
}
time.Sleep(500 * time.Millisecond)
}
return fmt.Errorf("timeout waiting for Claude to start")
}sendToTmuxFromTelegram function · go · L148-L151 (4 LOC)tmux.go
func sendToTmuxFromTelegram(session string, text string) error {
os.WriteFile(telegramActiveFlag(session), []byte("1"), 0600)
return sendToTmux(session, text)
}Want this analysis on your repo? https://repobility.com/scan/
sendToTmuxFromTelegramWithDelay function · go · L153-L156 (4 LOC)tmux.go
func sendToTmuxFromTelegramWithDelay(session string, text string, delay time.Duration) error {
os.WriteFile(telegramActiveFlag(session), []byte("1"), 0600)
return sendToTmuxWithDelay(session, text, delay)
}sendToTmux function · go · L158-L168 (11 LOC)tmux.go
func sendToTmux(session string, text string) error {
// Calculate delay based on text length
// Base: 50ms + 0.5ms per character, capped at 5 seconds
baseDelay := 50 * time.Millisecond
charDelay := time.Duration(len(text)) * 500 * time.Microsecond // 0.5ms per char
delay := baseDelay + charDelay
if delay > 5*time.Second {
delay = 5 * time.Second
}
return sendToTmuxWithDelay(session, text, delay)
}sendToTmuxWithDelay function · go · L170-L209 (40 LOC)tmux.go
func sendToTmuxWithDelay(session string, text string, delay time.Duration) error {
// Send text literally
cmd := exec.Command(tmuxPath, "send-keys", "-t", session, "-l", text)
if err := cmd.Run(); err != nil {
return err
}
// Wait for content to load (e.g., images)
time.Sleep(delay)
// Wait for "↵ send" indicator to appear (Claude Code is ready for Enter)
// Poll for up to 5 seconds
for i := 0; i < 50; i++ {
out, err := exec.Command(tmuxPath, "capture-pane", "-t", session, "-p", "-S", "-3").Output()
if err == nil && strings.Contains(string(out), "↵ send") {
break
}
time.Sleep(100 * time.Millisecond)
}
// Try sending Enter up to 3 times, checking if it was processed
for attempt := 0; attempt < 3; attempt++ {
// Send Enter twice (Claude Code needs double Enter)
exec.Command(tmuxPath, "send-keys", "-t", session, "C-m").Run()
time.Sleep(50 * time.Millisecond)
exec.Command(tmuxPath, "send-keys", "-t", session, "C-m").Run()
// Wait a bit and check if "↵killTmuxSession function · go · L211-L214 (4 LOC)tmux.go
func killTmuxSession(name string) error {
cmd := exec.Command(tmuxPath, "kill-session", "-t", name)
return cmd.Run()
}listTmuxSessions function · go · L216-L232 (17 LOC)tmux.go
func listTmuxSessions() ([]string, error) {
cmd := exec.Command(tmuxPath, "list-sessions", "-F", "#{session_name}")
out, err := cmd.Output()
if err != nil {
return nil, err
}
var sessions []string
scanner := bufio.NewScanner(bytes.NewReader(out))
for scanner.Scan() {
name := scanner.Text()
if strings.HasPrefix(name, "claude-") {
sessions = append(sessions, strings.TrimPrefix(name, "claude-"))
}
}
return sessions, nil
}getModelsDir function · go · L23-L26 (4 LOC)whisper.go
func getModelsDir() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".ccc", "models")
}ensureModel function · go · L29-L71 (43 LOC)whisper.go
func ensureModel() (string, error) {
modelsDir := getModelsDir()
modelPath := filepath.Join(modelsDir, whisperModelName)
if _, err := os.Stat(modelPath); err == nil {
return modelPath, nil
}
if err := os.MkdirAll(modelsDir, 0755); err != nil {
return "", fmt.Errorf("failed to create models dir: %w", err)
}
fmt.Printf("Downloading whisper model %s...\n", whisperModelName)
resp, err := http.Get(whisperModelURL)
if err != nil {
return "", fmt.Errorf("failed to download model: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", fmt.Errorf("failed to download model: HTTP %d", resp.StatusCode)
}
tmpPath := modelPath + ".tmp"
f, err := os.Create(tmpPath)
if err != nil {
return "", fmt.Errorf("failed to create model file: %w", err)
}
written, err := io.Copy(f, resp.Body)
f.Close()
if err != nil {
os.Remove(tmpPath)
return "", fmt.Errorf("failed to write model: %w", err)
}
if err := os.Rename(tmpPath, modelPath); err != nil {
ostranscribeAudio function · go · L74-L114 (41 LOC)whisper.go
func transcribeAudio(config *Config, audioPath string) (string, error) {
modelsDir := getModelsDir()
// Ensure model exists
if _, err := ensureModel(); err != nil {
return "", fmt.Errorf("model setup failed: %w", err)
}
manager, err := whisper.New(modelsDir)
if err != nil {
return "", fmt.Errorf("failed to create whisper manager: %w", err)
}
defer manager.Close()
model := manager.GetModelById("ggml-small")
if model == nil {
return "", fmt.Errorf("model ggml-small not found in %s", modelsDir)
}
var result strings.Builder
err = manager.WithModel(model, func(task *whisper.Task) error {
if config.TranscriptionLang != "" {
if err := task.SetLanguage(config.TranscriptionLang); err != nil {
return fmt.Errorf("failed to set language: %w", err)
}
}
f, err := os.Open(audioPath)
if err != nil {
return fmt.Errorf("failed to open audio: %w", err)
}
defer f.Close()
return task.TranscribeReader(context.Background(), f, func(seg *schema.Segment) {
rRepobility — the code-quality scanner for AI-generated software · https://repobility.com
doctorCheckWhisper function · go · L116-L125 (10 LOC)whisper.go
func doctorCheckWhisper() {
fmt.Print("whisper model..... ")
modelPath := filepath.Join(getModelsDir(), whisperModelName)
if _, err := os.Stat(modelPath); err == nil {
fmt.Printf("✅ %s\n", modelPath)
} else {
fmt.Println("⚠️ not downloaded (will auto-download on first voice message)")
fmt.Println(" Model: " + whisperModelName)
}
}transcribeAudio function · go · L10-L12 (3 LOC)whisper_stub.go
func transcribeAudio(config *Config, audioPath string) (string, error) {
return "", fmt.Errorf("voice transcription not available (build with: go build -tags voice)")
}doctorCheckWhisper function · go · L14-L16 (3 LOC)whisper_stub.go
func doctorCheckWhisper() {
fmt.Println("whisper........... ⚠️ not compiled (build with: go build -tags voice)")
}‹ prevpage 2 / 2