refactor(cli): migrate interactive prompts to @clack/prompts#185
Conversation
Greptile SummaryThis PR migrates ~1,500 lines of
Confidence Score: 2/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant tinyclaw.sh
participant BashLib as lib/*.sh
participant NodeCLI as dist/cli/*.js
participant Settings as settings.json
participant AgentDir as Agent Directories
User->>tinyclaw.sh: tinyclaw setup
tinyclaw.sh->>NodeCLI: node dist/cli/setup-wizard.js
NodeCLI->>Settings: writeSettings()
NodeCLI->>AgentDir: ensureAgentDirectory()
User->>tinyclaw.sh: tinyclaw agent add
tinyclaw.sh->>NodeCLI: node dist/cli/agent.js add
NodeCLI->>Settings: writeSettings()
NodeCLI->>AgentDir: ensureAgentDirectory()
User->>tinyclaw.sh: tinyclaw agent remove <id>
tinyclaw.sh->>NodeCLI: node dist/cli/agent.js remove <id>
NodeCLI->>Settings: writeSettings()
NodeCLI->>AgentDir: fs.rmSync()
User->>tinyclaw.sh: tinyclaw team add
tinyclaw.sh->>NodeCLI: node dist/cli/team.js add
NodeCLI->>Settings: writeSettings()
NodeCLI->>AgentDir: refreshTeamInfo() → updateAgentTeammates()
User->>tinyclaw.sh: tinyclaw team add-agent <team> <agent>
tinyclaw.sh->>BashLib: team_add_agent()
BashLib->>Settings: jq mutation
Note over BashLib,AgentDir: ⚠️ AGENTS.md NOT updated (regression)
User->>tinyclaw.sh: tinyclaw update
tinyclaw.sh->>NodeCLI: node dist/cli/update.js
NodeCLI->>NodeCLI: getLatestVersion() via curl+GitHub API
NodeCLI->>AgentDir: execSync(curl + tar + cp)
Last reviewed commit: 3427955 |
| team.agents = remaining; | ||
| team.leader_agent = newLeader; | ||
| writeSettings(settings); | ||
|
|
||
| refreshTeamInfo(settings); | ||
|
|
||
| p.log.success(`Removed @${agentId} from team '${teamId}'.${newLeader !== team.leader_agent ? ` New leader: @${newLeader}.` : ''}`); |
There was a problem hiding this comment.
New-leader message is never shown
team.leader_agent is mutated to newLeader on line 162, so by the time the success message is evaluated on line 167, the comparison newLeader !== team.leader_agent is always false. The "New leader: @..." note will never appear in the output, even when the leadership did change.
You need to capture the original leader before mutating the object:
| team.agents = remaining; | |
| team.leader_agent = newLeader; | |
| writeSettings(settings); | |
| refreshTeamInfo(settings); | |
| p.log.success(`Removed @${agentId} from team '${teamId}'.${newLeader !== team.leader_agent ? ` New leader: @${newLeader}.` : ''}`); | |
| const previousLeader = team.leader_agent; | |
| team.agents = remaining; | |
| team.leader_agent = newLeader; | |
| writeSettings(settings); | |
| refreshTeamInfo(settings); | |
| p.log.success(`Removed @${agentId} from team '${teamId}'.${newLeader !== previousLeader ? ` New leader: @${newLeader}.` : ''}`); |
| @@ -314,192 +111,5 @@ team_add_agent() { | |||
| local team_name | |||
| team_name=$(jq -r "(.teams // {}).\"${team_id}\".name // \"${team_id}\"" "$SETTINGS_FILE" 2>/dev/null) | |||
|
|
|||
| # Update AGENTS.md for all members in this team. | |||
| while IFS= read -r aid; do | |||
| update_agent_team_info "$aid" | |||
| done < <(jq -r "(.teams // {}).\"${team_id}\".agents[]" "$SETTINGS_FILE" 2>/dev/null) | |||
|
|
|||
| echo -e "${GREEN}Added @${agent_id} to team '${team_id}' (${team_name}).${NC}" | |||
| } | |||
There was a problem hiding this comment.
team add-agent no longer syncs AGENTS.md
The update_agent_team_info call — which kept every team member's AGENTS.md file in sync with the team collaboration section — was removed from team_add_agent, and the function itself was deleted from this file. Because tinyclaw team add-agent <team_id> <agent_id> still dispatches to this bash function (it was not delegated to TypeScript), calling it will silently skip the AGENTS.md update step.
The result is that agents' context files fall out of sync (new teammates aren't listed, existing agents don't learn they have a new peer) until the user manually triggers one of the TypeScript-backed commands that calls refreshTeamInfo.
To fix this, either:
- Delegate
team add-agentto the TypeScript CLI and have it callrefreshTeamInfo, or - Re-add the equivalent bash logic by invoking the
updateAgentTeammateshelper via a small Node one-liner after thejqmutation.
| const tempDir = execSync('mktemp -d', { encoding: 'utf8' }).trim(); | ||
| const bundleUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${latestVersion}/tinyclaw-bundle.tar.gz`; | ||
| try { | ||
| execSync(`curl -fSL -o "${tempDir}/tinyclaw-bundle.tar.gz" "${bundleUrl}"`, { stdio: 'ignore' }); |
There was a problem hiding this comment.
Unsanitized latestVersion interpolated into a shell command
latestVersion is taken directly from the GitHub API response (data.tag_name.replace(/^v/, '')) and then embedded verbatim into a shell template literal. Only the leading v is stripped — no other sanitization is applied. If the API ever returns a crafted tag_name (e.g., through a GitHub compromise or a MITM attack), an attacker could inject arbitrary shell commands via characters like ", $(), or ` inside the version string.
// Vulnerable pattern:
execSync(`curl -fSL -o "${tempDir}/tinyclaw-bundle.tar.gz" "${bundleUrl}"`, { stdio: 'ignore' });Prefer passing arguments as an array to avoid the shell entirely, or at minimum validate that latestVersion matches a strict semver pattern before using it in any command:
if (!/^\d+\.\d+(\.\d+)?$/.test(latestVersion)) {
p.log.error('Unexpected version format from GitHub API; aborting.');
return;
}| const workspacePath = settings.workspace?.path || ''; | ||
| const agentDir = path.join(workspacePath, agentId); | ||
| if (fs.existsSync(agentDir)) { | ||
| fs.rmSync(agentDir, { recursive: true, force: true }); | ||
| } |
There was a problem hiding this comment.
Inconsistent fallback for workspacePath in agentRemove may leave agent directory undeleted
In agentAdd (line 77), the fallback for a missing workspace path is path.join(process.env.HOME || '~', 'tinyclaw-workspace') — a concrete, usable path. But in agentRemove (line 148) the fallback is an empty string ''. If the settings file genuinely lacks a workspace.path key, path.join('', agentId) resolves to just the bare agent ID (e.g. "coder"), which is a relative path from the current working directory. fs.existsSync("coder") will be false in virtually any normal cwd, so the directory will be silently skipped and left on disk.
Use the same default as agentAdd:
| const workspacePath = settings.workspace?.path || ''; | |
| const agentDir = path.join(workspacePath, agentId); | |
| if (fs.existsSync(agentDir)) { | |
| fs.rmSync(agentDir, { recursive: true, force: true }); | |
| } | |
| const workspacePath = settings.workspace?.path || path.join(process.env.HOME || '~', 'tinyclaw-workspace'); | |
| const agentDir = path.join(workspacePath, agentId); |
Issues Fixed: - React error TinyAGI#185 (Maximum update depth exceeded) caused by: 1. Zustand selectors returning new function references on every render 2. useEffect dependencies triggering re-renders in a loop 3. React Query + Zustand interaction causing cascading updates Solutions Applied: - Refactored useClawStore to remove computed properties (getFilteredAgents, etc) - Created separate selector hooks: useFilteredAgents, useAgentById, useTeamById - Used Zustand getState() in useSSE to avoid subscription re-renders - Simplified page.tsx to remove problematic useEffect for view mode - Used refs in DataProvider to prevent re-render cascades UI Verified: - Header with logo, view toggle, stats, search, new agent button - Empty state displays correctly when no API connection - Dark theme with glassmorphism cards rendering properly Screenshot: screenshots/01-base-grid.png
…pt CLI Replace bash read-based interactive prompts with modern @clack/prompts library by creating TypeScript CLI modules. This improves UX with better validation, colored output, and structured interactions. - Create src/cli/ with setup-wizard.ts, agent.ts, team.ts, update.ts, shared.ts - Delegate interactive commands from tinyclaw.sh to Node CLI entry points - Remove interactive functions from bash (agent_add, agent_remove, team_add, team_remove, custom_provider_add/remove, do_update) - Keep non-interactive read-only commands in bash (list, show, provider, reset) - Add @clack/prompts dependency to package.json Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Relocate src/cli/ to packages/cli/ as @tinyclaw/cli workspace package, update imports from ../lib/* to @tinyclaw/core, and fix tinyclaw.sh dispatch paths to packages/cli/dist/. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
3427955 to
623ad1c
Compare
…pt CLI Move agent, team, pairing, messaging, and provider commands to @tinyclaw/cli using @clack/prompts for output formatting. Consolidate remaining bash helpers (update checks, agent skills sync, log viewing) into lib/daemon.sh and delete lib/agents.sh, lib/messaging.sh, lib/update.sh, lib/pairing.sh, lib/teams.sh. tinyclaw.sh is now a thin dispatcher that delegates all user commands to `node packages/cli/dist/<module>.js`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Revert merging update functions into daemon.sh — keep lib/update.sh as its own file. Add `tinyclaw version` (also --version, -v) command that prints the current version from package.json. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Description
Migrate all interactive bash prompts to TypeScript using the @clack/prompts library. This replaces 1500+ lines of bash read-based prompts with modern, validated TypeScript CLI modules, improving UX with better validation feedback, colored output, and structured interactions.
Changes
src/cli/with 5 new TypeScript modules:setup-wizard.ts: Full setup flow with channel selection, provider/model selection, agent configurationagent.ts: Agent add/remove, custom provider add/remove with interactive dialogsteam.ts: Team creation/removal, multi-select agent assignment, leader selectionupdate.ts: Update confirmation and progress tracking with spinnersshared.ts: Shared helpers (unwrap, cleanId, validators, settings I/O, option builders)tinyclaw.shto delegate interactive commands tonode dist/cli/*.jslib/agents.sh,lib/teams.sh,lib/update.sh@clack/promptsto dependenciesTesting
npm run build:main✓dist/cli/✓Checklist