Skip to content

refactor(cli): migrate interactive prompts to @clack/prompts#185

Merged
jlia0 merged 5 commits intomainfrom
jlia0/clack-prompts-migration
Mar 11, 2026
Merged

refactor(cli): migrate interactive prompts to @clack/prompts#185
jlia0 merged 5 commits intomainfrom
jlia0/clack-prompts-migration

Conversation

@jlia0
Copy link
Copy Markdown
Collaborator

@jlia0 jlia0 commented Mar 9, 2026

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

  • Created src/cli/ with 5 new TypeScript modules:
    • setup-wizard.ts: Full setup flow with channel selection, provider/model selection, agent configuration
    • agent.ts: Agent add/remove, custom provider add/remove with interactive dialogs
    • team.ts: Team creation/removal, multi-select agent assignment, leader selection
    • update.ts: Update confirmation and progress tracking with spinners
    • shared.ts: Shared helpers (unwrap, cleanId, validators, settings I/O, option builders)
  • Updated tinyclaw.sh to delegate interactive commands to node dist/cli/*.js
  • Removed ~1500 lines of bash interactive logic from lib/agents.sh, lib/teams.sh, lib/update.sh
  • Added @clack/prompts to dependencies

Testing

  • TypeScript compiles without errors: npm run build:main
  • All bash syntax is valid (agents.sh, teams.sh, update.sh) ✓
  • CLI modules build correctly to dist/cli/
  • Existing bash read-only commands remain functional ✓

Checklist

  • PR title follows conventional commit format
  • Code compiles and builds successfully
  • Bash syntax is valid
  • Interactive commands properly delegated to TypeScript
  • Maintained backward compatibility for non-interactive commands

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 9, 2026

Greptile Summary

This PR migrates ~1,500 lines of bash read-based interactive prompts into five TypeScript modules under src/cli/, using @clack/prompts for a richer UX. The overall architecture is sound — tinyclaw.sh dispatches interactive commands to the compiled JS modules while keeping non-interactive bash helpers in place — and the shared helpers in shared.ts are clean and well-reused. However, there are three correctness regressions and one security issue introduced in the migration:

  • src/cli/team.ts — The "New leader" annotation in teamRemoveAgent's success message is never displayed because team.leader_agent is mutated before the conditional comparison is evaluated.
  • lib/teams.shteam_add_agent (still a bash function, not delegated to TypeScript) had its update_agent_team_info call removed alongside the deleted function, breaking AGENTS.md sync for the tinyclaw team add-agent subcommand.
  • src/cli/update.tslatestVersion from the GitHub API is interpolated verbatim into execSync shell commands without validating its format, leaving a command injection surface.
  • src/cli/agent.tsagentRemove falls back to an empty string for the workspace path (while agentAdd correctly uses the home directory), causing the agent's directory to silently not be cleaned up when workspace.path is missing from settings.

Confidence Score: 2/5

  • Not safe to merge — contains a functional regression in team agent sync, a logic bug in the leader-change message, an inconsistent directory-deletion fallback, and an unsanitized shell interpolation in the update path.
  • Three of the four issues will silently produce wrong behaviour at runtime: AGENTS.md files won't stay in sync after team add-agent, the "New leader" message is structurally broken, and agent directories may not be cleaned up after removal. The shell injection in update.ts is a lower-probability but higher-severity security concern. The remaining files are clean and the overall refactoring direction is good.
  • src/cli/update.ts, lib/teams.sh, src/cli/team.ts, and src/cli/agent.ts need attention before merging.

Important Files Changed

Filename Overview
src/cli/agent.ts New TypeScript CLI for agent add/remove and custom provider add/remove; mostly correct but agentRemove uses an empty string fallback for workspace path, risking silent failure to clean up the agent directory.
src/cli/team.ts New TypeScript CLI for team management; teamRemoveAgent contains a logic bug where the "New leader" success message is never shown because team.leader_agent is mutated before the comparison is evaluated.
src/cli/update.ts New TypeScript CLI for performing updates; latestVersion from the GitHub API is interpolated into execSync shell commands without sanitization, creating a potential command injection vector.
src/cli/setup-wizard.ts Clean TypeScript replacement for the setup wizard bash script; logic is well-structured and faithfully reproduces the original flow using @clack/prompts.
src/cli/shared.ts Shared helpers for all CLI modules; unwrap, cleanId, validateId, and writeSettings are well-implemented and reused consistently.
lib/teams.sh Interactive functions correctly removed, but team_add_agent lost its update_agent_team_info call and the function itself was deleted, so tinyclaw team add-agent no longer keeps AGENTS.md files in sync.
tinyclaw.sh Cleanly delegates all interactive commands to the appropriate dist/cli/*.js modules; non-interactive bash commands are untouched.

Sequence Diagram

sequenceDiagram
    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)
Loading

Last reviewed commit: 3427955

Comment thread packages/cli/src/team.ts
Comment on lines +161 to +167
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}.` : ''}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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}.` : ''}`);

Comment thread lib/teams.sh Outdated
Comment on lines 65 to 115
@@ -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}"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-agent to the TypeScript CLI and have it call refreshTeamInfo, or
  • Re-add the equivalent bash logic by invoking the updateAgentTeammates helper via a small Node one-liner after the jq mutation.

Comment on lines +107 to +110
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' });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
}

Comment thread packages/cli/src/agent.ts
Comment on lines +148 to +152
const workspacePath = settings.workspace?.path || '';
const agentDir = path.join(workspacePath, agentId);
if (fs.existsSync(agentDir)) {
fs.rmSync(agentDir, { recursive: true, force: true });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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);

dpbmaverick98 pushed a commit to dpbmaverick98/tinyclaw that referenced this pull request Mar 10, 2026
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
jlia0 and others added 2 commits March 11, 2026 05:35
…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>
jlia0 and others added 3 commits March 11, 2026 15:32
…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>
@jlia0 jlia0 merged commit 1eb71f7 into main Mar 11, 2026
@jlia0 jlia0 deleted the jlia0/clack-prompts-migration branch March 11, 2026 07:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant