From a7a145784c99a151f472d764983fe0749f2a084a Mon Sep 17 00:00:00 2001 From: MarvelNwachukwu Date: Thu, 19 Feb 2026 12:16:29 +0000 Subject: [PATCH 1/4] docs: remove stale SessionManager and memory template references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SessionManager.ts was deleted but its entry remained in CODEBASE.md. Also corrected CLAUDE.md: memory starter files (facts.md etc.) do not exist as templates — ADK MemoryService creates them automatically. Removed outdated Known Issues entry about memoryTools.ts. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 6 ++---- CODEBASE.md | 23 +++++------------------ 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7b00f62..f97a251 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,8 +35,7 @@ adk-claw/ │ ├── AGENTS.md # Agent configuration template │ ├── SOUL.md # Agent personality template │ ├── USER.md # User profile template -│ ├── .gitignore -│ └── memory/ # Memory file templates +│ └── .gitignore ├── dist/ # Compiled output (includes templates after build) └── .adk-claw/ # User's local config (created by init, gitignored) ├── config.json # User configuration (API keys, etc.) @@ -59,7 +58,7 @@ pnpm lint:fix # Fix linting issues 3. Creates `.adk-claw/config.json` with all settings 4. Copies templates from `templates/workspace/` to `.adk-claw/workspace/`: - AGENTS.md, SOUL.md, USER.md (with placeholder replacement) - - memory/facts.md, context.md, preferences.md + - Creates empty `memory/` and `skills/` directories (ADK MemoryService creates memory files automatically) 5. Initializes git in workspace 6. User runs `adk-claw start` to launch the agent @@ -108,7 +107,6 @@ This copies templates to `dist/templates/` so the built CLI can find them. ## Known Issues / TODOs - The `@iqai/adk` package may show warnings about missing CLI files (can be ignored) -- Memory tools in `src/tools/memoryTools.ts` has uncommitted changes ## Tips for Future Sessions diff --git a/CODEBASE.md b/CODEBASE.md index 3f731c4..e3deae0 100644 --- a/CODEBASE.md +++ b/CODEBASE.md @@ -145,29 +145,17 @@ Telegram Flow: - **Exports**: `startTelegramBot()` - **Flow**: 1. Creates agent with `createClawdAgent({ channel: "telegram" })` - 2. Wires SessionManager with memory service - 3. Creates sampling handler that: + 2. Creates sampling handler that: - Parses incoming MCP messages (`parseTelegramMessage()`) - Routes `/start`, `/new`, `/reset`, `/help` commands - Checks pairing for non-command messages - Forwards to `runner.ask()` for AI responses - 4. Initializes Telegram agent (MCP tools) - 5. Registers bot commands with Telegram API - 6. Initializes scheduler + 3. Initializes Telegram agent (MCP tools) + 4. Registers bot commands with Telegram API + 5. Initializes scheduler - **Message format**: Key-value format from MCP (user_id, chat_id, content fields) - **Bot commands**: `/start`, `/new` (save & reset), `/reset` (clear), `/help` -### `src/services/SessionManager.ts` (Session State) -- **Purpose**: Per-chat session tracking with memory persistence -- **Exports**: `sessionManager` (singleton instance) -- **Class**: `SessionManager` - - `setDeps(memoryService, sessionService, adkSession)` — late-bound dependencies - - `getOrCreate(chatId, userId, username?)` → Session - - `addMessage(chatId, role, content)` - - `saveAndReset(chatId)` → summary string (saves to memory via ADK) - - `reset(chatId)` — clears without saving - - `getHistory(chatId)` / `getRecentHistory(chatId, count?)` - ### `src/services/SchedulerService.ts` (Cron Jobs) - **Purpose**: Manages scheduled/recurring tasks via `@iqai/adk` `AgentScheduler` - **Exports**: @@ -179,7 +167,7 @@ Telegram Flow: - **Key**: Jobs are configured in `config.json` cron section, results broadcast to all paired Telegram users ### `src/services/index.ts` (Services Barrel) -- **Re-exports**: `initScheduler`, `stopScheduler`, `sessionManager`, `startTelegramBot` +- **Re-exports**: `initScheduler`, `stopScheduler`, `startTelegramBot` --- @@ -361,7 +349,6 @@ src/services/TelegramService.ts ├── lib/logger.ts, lib/pairing.ts ├── tools/scheduleTools.ts (setSchedulerDeps) ├── services/SchedulerService.ts - └── services/SessionManager.ts src/agents/agent.ts ├── config/index.ts (getConfig) From 84fb8a53dd2033c6eb7abd5e2d1cf31a4c1fb3bd Mon Sep 17 00:00:00 2001 From: MarvelNwachukwu Date: Thu, 19 Feb 2026 12:16:36 +0000 Subject: [PATCH 2/4] refactor: remove dead code and simplify skill config mutations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete buildSystemPromptWithSkills() — skill prompt building is done inline in agent.ts; function was unused - Refactor addInstalledSkill/removeInstalledSkill to use updateConfig() instead of manual readFileSync/JSON.parse/writeFileSync blocks - Replace hardcoded test prompt in src/index.ts with a brief message directing users to `adk-claw chat` or `adk-claw start` Co-Authored-By: Claude Sonnet 4.6 --- src/agents/config/agent.config.ts | 17 ----------- src/config/index.ts | 48 ++++++++----------------------- src/index.ts | 35 +++------------------- 3 files changed, 16 insertions(+), 84 deletions(-) diff --git a/src/agents/config/agent.config.ts b/src/agents/config/agent.config.ts index 88fe92f..41dff27 100644 --- a/src/agents/config/agent.config.ts +++ b/src/agents/config/agent.config.ts @@ -8,7 +8,6 @@ import * as path from "node:path"; import matter from "gray-matter"; import { configExists, getConfig } from "../../config/index.js"; import { HEARTBEAT_OK, SILENT_REPLY_TOKEN } from "../../lib/tokens.js"; -import { buildSkillsPrompt } from "../../skills/SkillService.js"; import type { AgentsConfig, RuntimeInfo, @@ -396,22 +395,6 @@ export function buildSystemPromptFromParams( return buildSystemPrompt(params.workspaceConfig, params.runtime); } -/** - * Build the full system prompt with skills included (async version) - */ -export async function buildSystemPromptWithSkills( - config: WorkspaceConfig, -): Promise { - const basePrompt = buildSystemPrompt(config); - const skillsPrompt = await buildSkillsPrompt(config.workspacePath); - - if (skillsPrompt) { - return `${basePrompt}\n\n${skillsPrompt}`; - } - - return basePrompt; -} - /** * Check if workspace exists and has required files */ diff --git a/src/config/index.ts b/src/config/index.ts index 3b7838a..f7330eb 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -3,9 +3,10 @@ * Reads from .adk-claw/config.json */ -import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { z } from "zod"; +import { updateConfig } from "../lib/configWriter.js"; const CONFIG_PATH = join(process.cwd(), ".adk-claw", "config.json"); @@ -219,27 +220,11 @@ export function addInstalledSkill(skillName: string): void { if (!existsSync(CONFIG_PATH)) { return; } - - const raw = readFileSync(CONFIG_PATH, "utf-8"); - const config = JSON.parse(raw); - - if (!config.skills) { - config.skills = { - enabled: true, - directory: ".adk-claw/workspace/skills", - installed: [], - }; - } - if (!Array.isArray(config.skills.installed)) { - config.skills.installed = []; - } - - if (!config.skills.installed.includes(skillName)) { - config.skills.installed.push(skillName); - config.meta = config.meta || {}; - config.meta.lastUpdatedAt = new Date().toISOString(); - writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2)); - } + updateConfig((config) => { + if (!config.skills.installed.includes(skillName)) { + config.skills.installed.push(skillName); + } + }); } /** @@ -249,20 +234,11 @@ export function removeInstalledSkill(skillName: string): void { if (!existsSync(CONFIG_PATH)) { return; } - - const raw = readFileSync(CONFIG_PATH, "utf-8"); - const config = JSON.parse(raw); - - if (!config.skills?.installed) { - return; - } - - config.skills.installed = config.skills.installed.filter( - (s: string) => s !== skillName, - ); - config.meta = config.meta || {}; - config.meta.lastUpdatedAt = new Date().toISOString(); - writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2)); + updateConfig((config) => { + config.skills.installed = config.skills.installed.filter( + (s) => s !== skillName, + ); + }); } /** diff --git a/src/index.ts b/src/index.ts index e2b8fe5..8546beb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,36 +1,9 @@ -import { createClawdAgent } from "./agents/agent.js"; -import { configExists, getConfig } from "./config/index.js"; - /** * ADK Claw - Personal AI Assistant * - * Features persistent memory, configurable personality, and extensible skills. + * Run `adk-claw chat` to start an interactive chat session. + * Run `adk-claw start` to launch the Telegram bot. + * Run `adk-claw --help` to see all available commands. */ -async function main() { - // Check if config exists - if (!configExists()) { - console.error( - "❌ No config found. Run 'adk-claw init' to set up your project.", - ); - process.exit(1); - } - - const appConfig = getConfig(); - console.log(`🤖 Initializing ${appConfig.agentName}...\n`); - - try { - // Create the agent with CLI channel context - const { runner } = await createClawdAgent({ channel: "cli" }); - - const response = await runner.ask( - "Hello, how are you? I want to see something, a story, something random to memory...don't ask a question to clarify...just take an action", - ); - console.log(`\n${appConfig.agentName}: ${response}\n`); - } catch (error) { - console.error("❌ Failed to initialize:", error); - process.exit(1); - } -} - -main().catch(console.error); +console.log("Use `adk-claw chat` or `adk-claw start` to run ADK Claw."); From a1a444e2e2a2b75775a936c3777b1528b99c78f9 Mon Sep 17 00:00:00 2001 From: MarvelNwachukwu Date: Thu, 19 Feb 2026 12:16:41 +0000 Subject: [PATCH 3/4] refactor: extract OPENROUTER_MODELS to shared lib/models.ts Moves the model list out of init.ts into src/lib/models.ts so the upcoming config command can reuse it without duplication. Co-Authored-By: Claude Sonnet 4.6 --- src/cli/init.ts | 13 +------------ src/lib/models.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 12 deletions(-) create mode 100644 src/lib/models.ts diff --git a/src/cli/init.ts b/src/cli/init.ts index d8f76ba..1365836 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -4,6 +4,7 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import * as p from "@clack/prompts"; import pc from "picocolors"; +import { OPENROUTER_MODELS } from "../lib/models.js"; // Project-based paths (current working directory) const PROJECT_DIR = process.cwd(); @@ -69,18 +70,6 @@ interface Config { }; } -const OPENROUTER_MODELS = [ - { - value: "anthropic/claude-sonnet-4", - label: "Claude Sonnet 4 (Recommended)", - }, - { value: "anthropic/claude-3.5-sonnet", label: "Claude 3.5 Sonnet" }, - { value: "openai/gpt-4o", label: "GPT-4o" }, - { value: "openai/gpt-4o-mini", label: "GPT-4o Mini" }, - { value: "google/gemini-2.0-flash-001", label: "Gemini 2.0 Flash" }, - { value: "meta-llama/llama-3.3-70b-instruct", label: "Llama 3.3 70B" }, -]; - export async function init() { console.clear(); diff --git a/src/lib/models.ts b/src/lib/models.ts new file mode 100644 index 0000000..d8b61a5 --- /dev/null +++ b/src/lib/models.ts @@ -0,0 +1,15 @@ +/** + * Shared OpenRouter model list used by init and config commands + */ + +export const OPENROUTER_MODELS = [ + { + value: "anthropic/claude-sonnet-4", + label: "Claude Sonnet 4 (Recommended)", + }, + { value: "anthropic/claude-3.5-sonnet", label: "Claude 3.5 Sonnet" }, + { value: "openai/gpt-4o", label: "GPT-4o" }, + { value: "openai/gpt-4o-mini", label: "GPT-4o Mini" }, + { value: "google/gemini-2.0-flash-001", label: "Gemini 2.0 Flash" }, + { value: "meta-llama/llama-3.3-70b-instruct", label: "Llama 3.3 70B" }, +]; From 7aad19e82f107a4bc7a1b95369e71eacff06269f Mon Sep 17 00:00:00 2001 From: MarvelNwachukwu Date: Thu, 19 Feb 2026 12:16:49 +0000 Subject: [PATCH 4/4] feat(cli): add chat, config, and doctor commands - chat: interactive terminal loop with /help, /new, /exit slash commands - config show: prints current config with secrets masked (last 4 chars) - config set model|api-key|bot-token: interactive update via prompts - doctor: 11 health checks (config, workspace, API key formats, Telegram and OpenRouter connectivity); exits 1 on critical failures - Update splash screen with the three new commands - Fix Docs URL (was same as GitHub; now points to #readme anchor) Co-Authored-By: Claude Sonnet 4.6 --- src/cli/chat.ts | 98 ++++++++++++++++++ src/cli/config.ts | 193 ++++++++++++++++++++++++++++++++++ src/cli/doctor.ts | 258 ++++++++++++++++++++++++++++++++++++++++++++++ src/cli/index.ts | 42 ++++++++ 4 files changed, 591 insertions(+) create mode 100644 src/cli/chat.ts create mode 100644 src/cli/config.ts create mode 100644 src/cli/doctor.ts diff --git a/src/cli/chat.ts b/src/cli/chat.ts new file mode 100644 index 0000000..82c6fed --- /dev/null +++ b/src/cli/chat.ts @@ -0,0 +1,98 @@ +/** + * CLI command for interactive terminal chat with the agent + */ + +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import * as p from "@clack/prompts"; +import pc from "picocolors"; + +const CONFIG_PATH = join(process.cwd(), ".adk-claw", "config.json"); + +export async function chat() { + if (!existsSync(CONFIG_PATH)) { + p.log.error("ADK Claw is not initialized. Run 'adk-claw init' first."); + process.exit(1); + } + + const s = p.spinner(); + s.start("Initializing agent..."); + + const { createClawdAgent } = await import("../agents/agent.js"); + const { getConfig } = await import("../config/index.js"); + + let agentResult: Awaited>; + let agentName: string; + + try { + agentResult = await createClawdAgent({ channel: "cli" }); + agentName = getConfig().agentName; + s.stop(`${pc.cyan(agentName)} is ready`); + } catch (err) { + s.stop("Failed to initialize agent"); + p.log.error(err instanceof Error ? err.message : "Unknown error"); + process.exit(1); + } + + p.intro( + `${pc.bold(pc.magenta(agentName))} ${pc.dim("type /help for commands, /exit to quit")}`, + ); + + while (true) { + const input = await p.text({ + message: pc.cyan("You:"), + placeholder: "Say something...", + }); + + if (p.isCancel(input)) { + break; + } + + const trimmed = (input as string).trim(); + + if (!trimmed) { + continue; + } + + if (trimmed === "/exit" || trimmed === "/quit") { + break; + } + + if (trimmed === "/help") { + console.log(` + ${pc.bold("Commands:")} + /help Show this help + /new Start a fresh session + /exit Quit the chat +`); + continue; + } + + if (trimmed === "/new") { + const newS = p.spinner(); + newS.start("Starting new session..."); + try { + agentResult = await createClawdAgent({ channel: "cli" }); + newS.stop("New session started"); + } catch (err) { + newS.stop("Failed to start new session"); + p.log.error(err instanceof Error ? err.message : "Unknown error"); + } + continue; + } + + const thinkS = p.spinner(); + thinkS.start("Thinking..."); + + try { + const response = await agentResult.runner.ask(trimmed); + thinkS.stop(""); + console.log(`\n ${pc.bold(pc.magenta(agentName))}: ${response}\n`); + } catch (err) { + thinkS.stop("Error"); + p.log.error(err instanceof Error ? err.message : "Unknown error"); + } + } + + p.outro("Goodbye!"); +} diff --git a/src/cli/config.ts b/src/cli/config.ts new file mode 100644 index 0000000..68f741a --- /dev/null +++ b/src/cli/config.ts @@ -0,0 +1,193 @@ +/** + * CLI command for viewing and updating configuration + */ + +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { updateConfig } from "../lib/configWriter.js"; +import { OPENROUTER_MODELS } from "../lib/models.js"; + +const CONFIG_PATH = join(process.cwd(), ".adk-claw", "config.json"); + +function maskSecret(value: string): string { + if (!value || value.length <= 4) return "****"; + return `${"*".repeat(value.length - 4)}${value.slice(-4)}`; +} + +async function showConfig() { + if (!existsSync(CONFIG_PATH)) { + p.log.error("ADK Claw is not initialized. Run 'adk-claw init' first."); + process.exit(1); + } + + const { getRawConfig } = await import("../config/index.js"); + const config = getRawConfig(); + + p.intro(pc.bgCyan(pc.black(" ADK Claw Configuration "))); + + console.log(` ${pc.bold("Agent")}`); + console.log(` Name: ${pc.cyan(config.agent.name)}`); + console.log(` Workspace: ${pc.dim(config.agent.workspace)}`); + console.log(); + console.log(` ${pc.bold("Auth")}`); + console.log(` Provider: ${pc.cyan(config.auth.provider)}`); + console.log(` Model: ${pc.cyan(config.auth.model)}`); + console.log(` API Key: ${pc.dim(maskSecret(config.auth.apiKey))}`); + console.log(); + console.log(` ${pc.bold("Telegram")}`); + console.log( + ` Enabled: ${config.channels.telegram.enabled ? pc.green("yes") : pc.red("no")}`, + ); + console.log( + ` Bot Token: ${pc.dim(maskSecret(config.channels.telegram.botToken))}`, + ); + console.log(` DM Policy: ${pc.dim(config.channels.telegram.dmPolicy)}`); + console.log(); + console.log(` ${pc.bold("Skills")}`); + console.log( + ` Installed: ${config.skills.installed.length > 0 ? pc.cyan(config.skills.installed.join(", ")) : pc.dim("none")}`, + ); + console.log(); + console.log(pc.dim(` Updated: ${config.meta.lastUpdatedAt}`)); + + p.outro(pc.dim("Use 'adk-claw config set ' to update values.")); +} + +async function setModel() { + if (!existsSync(CONFIG_PATH)) { + p.log.error("ADK Claw is not initialized. Run 'adk-claw init' first."); + process.exit(1); + } + + const model = await p.select({ + message: "Select a model:", + options: OPENROUTER_MODELS, + }); + + if (p.isCancel(model)) { + p.cancel("Cancelled."); + return; + } + + const s = p.spinner(); + s.start("Updating model..."); + updateConfig((config) => { + config.auth.model = model as string; + }); + s.stop(`Model updated to ${pc.cyan(model as string)}`); +} + +async function setApiKey() { + if (!existsSync(CONFIG_PATH)) { + p.log.error("ADK Claw is not initialized. Run 'adk-claw init' first."); + process.exit(1); + } + + const apiKey = await p.password({ + message: "Enter new OpenRouter API key:", + validate: (value) => { + if (!value || !value.trim()) return "API key is required"; + if (!value.startsWith("sk-")) + return "Invalid API key format (should start with sk-)"; + return undefined; + }, + }); + + if (p.isCancel(apiKey)) { + p.cancel("Cancelled."); + return; + } + + const s = p.spinner(); + s.start("Updating API key..."); + updateConfig((config) => { + config.auth.apiKey = apiKey; + }); + s.stop("API key updated"); +} + +async function setBotToken() { + if (!existsSync(CONFIG_PATH)) { + p.log.error("ADK Claw is not initialized. Run 'adk-claw init' first."); + process.exit(1); + } + + const botToken = await p.password({ + message: "Enter new Telegram bot token:", + validate: (value) => { + if (!value || !value.trim()) return "Bot token is required"; + if (!value.includes(":")) return "Invalid bot token format"; + return undefined; + }, + }); + + if (p.isCancel(botToken)) { + p.cancel("Cancelled."); + return; + } + + const s = p.spinner(); + s.start("Updating bot token..."); + updateConfig((config) => { + config.channels.telegram.botToken = botToken; + }); + s.stop("Bot token updated"); +} + +function printConfigHelp() { + console.log(` +${pc.bold("adk-claw config")} - View and update configuration + +${pc.bold("Usage:")} + adk-claw config [subcommand] + +${pc.bold("Subcommands:")} + show Print current config (secrets masked) + set model Change the AI model + set api-key Change the OpenRouter API key + set bot-token Change the Telegram bot token +`); +} + +export async function configCommand(args: string[]) { + const subcommand = args[0]; + + switch (subcommand) { + case "show": + case undefined: + await showConfig(); + break; + case "set": { + const field = args[1]; + switch (field) { + case "model": + await setModel(); + break; + case "api-key": + await setApiKey(); + break; + case "bot-token": + await setBotToken(); + break; + default: + p.log.error( + `Unknown field: ${field ?? "(none)"}. Valid fields: model, api-key, bot-token`, + ); + printConfigHelp(); + process.exit(1); + } + break; + } + case "help": + case "--help": + case "-h": + printConfigHelp(); + break; + default: + p.log.error(`Unknown config command: ${subcommand}`); + printConfigHelp(); + process.exit(1); + } +} diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts new file mode 100644 index 0000000..5f7e11f --- /dev/null +++ b/src/cli/doctor.ts @@ -0,0 +1,258 @@ +/** + * CLI command for running health checks on ADK Claw configuration + */ + +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import pc from "picocolors"; + +const PROJECT_DIR = process.cwd(); +const ADK_CLAW_DIR = join(PROJECT_DIR, ".adk-claw"); +const CONFIG_PATH = join(ADK_CLAW_DIR, "config.json"); +const WORKSPACE_PATH = join(ADK_CLAW_DIR, "workspace"); + +interface CheckResult { + label: string; + passed: boolean; + detail?: string; + critical: boolean; +} + +function check( + label: string, + passed: boolean, + critical: boolean, + detail?: string, +): CheckResult { + return { label, passed, critical, detail }; +} + +function printResult(result: CheckResult) { + const icon = result.passed ? pc.green("✓") : pc.red("✗"); + const label = result.passed ? result.label : pc.red(result.label); + const detail = result.detail ? pc.dim(` — ${result.detail}`) : ""; + console.log(` ${icon} ${label}${detail}`); +} + +export async function doctor() { + console.log(); + console.log(pc.bold(" ADK Claw Health Check")); + console.log(); + + const results: CheckResult[] = []; + + // 1. Config file exists + const configExists = existsSync(CONFIG_PATH); + results.push( + check( + "Config file exists", + configExists, + true, + configExists ? CONFIG_PATH : `Not found at ${CONFIG_PATH}`, + ), + ); + + // 2. Config is valid JSON + let rawConfig: Record | null = null; + if (configExists) { + try { + const { readFileSync } = await import("node:fs"); + rawConfig = JSON.parse(readFileSync(CONFIG_PATH, "utf-8")); + results.push(check("Config is valid JSON", true, true)); + } catch { + results.push( + check("Config is valid JSON", false, true, "JSON parse error"), + ); + } + } else { + results.push( + check("Config is valid JSON", false, true, "Skipped (no config file)"), + ); + } + + // 3. Config passes Zod schema validation + let parsedConfig: Awaited< + ReturnType + > | null = null; + if (rawConfig) { + try { + const { getRawConfig } = await import("../config/index.js"); + parsedConfig = getRawConfig(); + results.push(check("Config schema valid", true, true)); + } catch (err) { + results.push( + check( + "Config schema valid", + false, + true, + err instanceof Error ? err.message : "Schema validation failed", + ), + ); + } + } else { + results.push( + check("Config schema valid", false, true, "Skipped (no valid JSON)"), + ); + } + + // 4. Workspace directory exists + const workspaceExists = existsSync(WORKSPACE_PATH); + results.push( + check("Workspace directory exists", workspaceExists, true, WORKSPACE_PATH), + ); + + // 5. Required workspace files present + if (workspaceExists) { + const { validateWorkspace } = await import( + "../agents/config/agent.config.js" + ); + const validation = await validateWorkspace(WORKSPACE_PATH); + results.push( + check( + "Workspace files present (SOUL.md, AGENTS.md, USER.md)", + validation.valid, + true, + validation.valid + ? undefined + : `Missing: ${validation.missing.join(", ")}`, + ), + ); + } else { + results.push( + check( + "Workspace files present (SOUL.md, AGENTS.md, USER.md)", + false, + true, + "Skipped (no workspace)", + ), + ); + } + + // 6. memory/ directory exists + const memoryExists = existsSync(join(WORKSPACE_PATH, "memory")); + results.push(check("memory/ directory exists", memoryExists, false)); + + // 7. skills/ directory exists + const skillsExists = existsSync(join(WORKSPACE_PATH, "skills")); + results.push(check("skills/ directory exists", skillsExists, false)); + + // 8. API key format valid + const apiKey = parsedConfig?.auth?.apiKey ?? ""; + const apiKeyValid = apiKey.startsWith("sk-"); + results.push( + check( + "API key format valid (starts with sk-)", + apiKeyValid, + true, + apiKeyValid ? undefined : "Does not start with sk-", + ), + ); + + // 9. Telegram bot token format valid + const botToken = parsedConfig?.channels?.telegram?.botToken ?? ""; + const botTokenValid = botToken.includes(":"); + results.push( + check( + "Telegram bot token format valid (contains :)", + botTokenValid, + true, + botTokenValid ? undefined : "Does not contain :", + ), + ); + + // 10. Telegram API reachable + if (botTokenValid) { + try { + const res = await fetch(`https://api.telegram.org/bot${botToken}/getMe`); + const json = (await res.json()) as { ok: boolean; description?: string }; + results.push( + check( + "Telegram API reachable", + json.ok === true, + false, + json.ok ? undefined : (json.description ?? "API returned ok: false"), + ), + ); + } catch (err) { + results.push( + check( + "Telegram API reachable", + false, + false, + err instanceof Error ? err.message : "Network error", + ), + ); + } + } else { + results.push( + check( + "Telegram API reachable", + false, + false, + "Skipped (invalid bot token)", + ), + ); + } + + // 11. OpenRouter API reachable + if (apiKeyValid) { + try { + const res = await fetch("https://openrouter.ai/api/v1/models", { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }); + results.push( + check( + "OpenRouter API reachable", + res.ok, + false, + res.ok ? undefined : `HTTP ${res.status}`, + ), + ); + } catch (err) { + results.push( + check( + "OpenRouter API reachable", + false, + false, + err instanceof Error ? err.message : "Network error", + ), + ); + } + } else { + results.push( + check( + "OpenRouter API reachable", + false, + false, + "Skipped (invalid API key)", + ), + ); + } + + // Print all results + for (const result of results) { + printResult(result); + } + + console.log(); + + const passed = results.filter((r) => r.passed).length; + const total = results.length; + const criticalFailed = results.some((r) => r.critical && !r.passed); + + const summary = `${passed}/${total} checks passed`; + console.log( + ` ${criticalFailed ? pc.red(summary) : passed === total ? pc.green(summary) : pc.yellow(summary)}`, + ); + console.log(); + + if (criticalFailed) { + console.log( + pc.dim(" Run 'adk-claw init' to set up or fix your configuration."), + ); + console.log(); + process.exit(1); + } +} diff --git a/src/cli/index.ts b/src/cli/index.ts index 958a8cc..4824d1c 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -7,6 +7,39 @@ dotenv.config(); const args = process.argv.slice(2); const command = args[0]; +const LOGO = ` +${pc.magenta(" █████╗ ██████╗ ██╗ ██╗ ██████╗██╗ █████╗ ██╗ ██╗")} +${pc.magenta(" ██╔══██╗██╔══██╗██║ ██╔╝ ██╔════╝██║ ██╔══██╗██║ ██║")} +${pc.magenta(" ███████║██║ ██║█████╔╝ ██║ ██║ ███████║██║ █╗ ██║")} +${pc.magenta(" ██╔══██║██║ ██║██╔═██╗ ██║ ██║ ██╔══██║██║███╗██║")} +${pc.magenta(" ██║ ██║██████╔╝██║ ██╗ ╚██████╗███████╗██║ ██║╚███╔███╔╝")} +${pc.magenta(" ╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝")}`; + +function printSplash() { + console.log(LOGO); + console.log(); + console.log( + pc.dim(" Personal AI assistant with memory, Telegram & skills."), + ); + console.log(); + console.log(pc.dim(` v${VERSION}`)); + console.log(); + console.log(` ${pc.magenta("init")} Create a new ADK Claw project`); + console.log(` ${pc.magenta("start")} Start the AI agent`); + console.log( + ` ${pc.magenta("chat")} Chat with the agent in your terminal`, + ); + console.log(` ${pc.magenta("config")} View and update configuration`); + console.log(` ${pc.magenta("doctor")} Run health checks`); + console.log(` ${pc.magenta("skill")} Manage agent skills`); + console.log(` ${pc.magenta("service")} Manage the systemd user service`); + console.log(` ${pc.magenta("pairing")} Manage Telegram user pairing`); + console.log(); + console.log(pc.dim(` Docs: https://github.com/IQAIcom/adk-claw#readme`)); + console.log(pc.dim(` GitHub: https://github.com/IQAIcom/adk-claw`)); + console.log(); +} + async function main() { switch (command) { case "init": @@ -16,6 +49,15 @@ async function main() { case "start": await import("./start.js").then((m) => m.start()); break; + case "chat": + await import("./chat.js").then((m) => m.chat()); + break; + case "config": + await import("./config.js").then((m) => m.configCommand(args.slice(1))); + break; + case "doctor": + await import("./doctor.js").then((m) => m.doctor()); + break; case "skill": case "skills": await import("./skill.js").then((m) => m.skillCommand(args.slice(1)));