Skip to content

Commit 56c5625

Browse files
sionsmithclaude
andcommitted
feat: keito-cli v0.1.0 — initial release
Full Phase 1 implementation: auth, time tracking, projects, dual output mode (table/JSON), richer JSON errors with suggestion/details fields, and time stop --discard to abandon timers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0 parents  commit 56c5625

32 files changed

Lines changed: 4013 additions & 0 deletions

.github/workflows/auto-tag.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Auto Tag
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- Cargo.toml
8+
9+
jobs:
10+
tag:
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: write
14+
steps:
15+
- uses: actions/checkout@v4
16+
with:
17+
fetch-depth: 2
18+
persist-credentials: false
19+
20+
- name: Check version bump
21+
id: version
22+
run: |
23+
OLD_VERSION=$(git diff HEAD~1 HEAD -- Cargo.toml | grep '^-version' | head -1 | sed 's/.*"\(.*\)".*/\1/' || echo "")
24+
NEW_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
25+
if [ -n "$OLD_VERSION" ] && [ "$OLD_VERSION" != "$NEW_VERSION" ]; then
26+
echo "changed=true" >> "$GITHUB_OUTPUT"
27+
echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
28+
else
29+
echo "changed=false" >> "$GITHUB_OUTPUT"
30+
fi
31+
32+
- name: Check if tag exists
33+
if: steps.version.outputs.changed == 'true'
34+
id: tag-check
35+
run: |
36+
if git rev-parse "v${{ steps.version.outputs.version }}" >/dev/null 2>&1; then
37+
echo "exists=true" >> "$GITHUB_OUTPUT"
38+
else
39+
echo "exists=false" >> "$GITHUB_OUTPUT"
40+
fi
41+
42+
- name: Create and push tag
43+
if: steps.version.outputs.changed == 'true' && steps.tag-check.outputs.exists == 'false'
44+
env:
45+
# Use PAT instead of GITHUB_TOKEN so the tag push triggers the Release workflow.
46+
# Tags pushed by GITHUB_TOKEN don't trigger downstream workflows (GitHub Actions limitation).
47+
GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
48+
run: |
49+
git config user.name "github-actions[bot]"
50+
git config user.email "github-actions[bot]@users.noreply.github.com"
51+
git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git"
52+
git tag -a "v${{ steps.version.outputs.version }}" -m "Release v${{ steps.version.outputs.version }}"
53+
git push origin "v${{ steps.version.outputs.version }}"

.github/workflows/release.yml

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
name: Release
2+
3+
permissions:
4+
contents: write
5+
6+
on:
7+
push:
8+
tags:
9+
- v[0-9]+.*
10+
11+
jobs:
12+
create-release:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Extract changelog
18+
run: |
19+
VERSION="${GITHUB_REF_NAME#v}"
20+
awk "/^## \\[${VERSION}\\]/{found=1; next} /^## \\[/{if(found) exit} found{print}" CHANGELOG.md > release_notes.md
21+
22+
- name: Create GitHub release
23+
env:
24+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
25+
run: |
26+
gh release create "$GITHUB_REF_NAME" \
27+
--title "$GITHUB_REF_NAME" \
28+
--notes-file release_notes.md
29+
30+
upload-assets:
31+
needs: create-release
32+
strategy:
33+
matrix:
34+
include:
35+
- target: aarch64-apple-darwin
36+
os: macos-latest
37+
- target: x86_64-apple-darwin
38+
os: macos-latest
39+
- target: x86_64-unknown-linux-gnu
40+
os: ubuntu-latest
41+
- target: x86_64-pc-windows-msvc
42+
os: windows-latest
43+
runs-on: ${{ matrix.os }}
44+
defaults:
45+
run:
46+
shell: bash
47+
steps:
48+
- uses: actions/checkout@v4
49+
- uses: dtolnay/rust-toolchain@stable
50+
with:
51+
targets: ${{ matrix.target }}
52+
- uses: Swatinem/rust-cache@v2
53+
54+
- name: Build
55+
run: cargo build --release --target ${{ matrix.target }}
56+
57+
- name: Package (unix)
58+
if: runner.os != 'Windows'
59+
run: |
60+
cd target/${{ matrix.target }}/release
61+
tar czf ../../../keito-${{ matrix.target }}.tar.gz keito
62+
63+
- name: Package (windows)
64+
if: runner.os == 'Windows'
65+
shell: pwsh
66+
run: |
67+
cd target/${{ matrix.target }}/release
68+
7z a ../../../keito-${{ matrix.target }}.zip keito.exe
69+
70+
- name: Upload assets
71+
env:
72+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
73+
run: |
74+
gh release upload "$GITHUB_REF_NAME" keito-${{ matrix.target }}.* --clobber
75+
76+
homebrew:
77+
needs: upload-assets
78+
runs-on: ubuntu-latest
79+
steps:
80+
- name: Update Homebrew tap
81+
env:
82+
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
83+
run: |
84+
set -euo pipefail
85+
VERSION="${GITHUB_REF_NAME#v}"
86+
BASE_URL="https://github.com/osodevops/keito-cli/releases/download/${GITHUB_REF_NAME}"
87+
88+
# Download each binary tarball and compute SHA256
89+
curl -fsSL "${BASE_URL}/keito-aarch64-apple-darwin.tar.gz" -o arm64.tar.gz
90+
curl -fsSL "${BASE_URL}/keito-x86_64-apple-darwin.tar.gz" -o x86_64_mac.tar.gz
91+
curl -fsSL "${BASE_URL}/keito-x86_64-unknown-linux-gnu.tar.gz" -o x86_64_linux.tar.gz
92+
93+
SHA_ARM64=$(sha256sum arm64.tar.gz | awk '{print $1}')
94+
SHA_X86_64_MAC=$(sha256sum x86_64_mac.tar.gz | awk '{print $1}')
95+
SHA_X86_64_LINUX=$(sha256sum x86_64_linux.tar.gz | awk '{print $1}')
96+
97+
# Generate the formula
98+
cat > keito.rb <<FORMULA
99+
class Keito < Formula
100+
desc "CLI for AI agents and humans to track billable time against Keito"
101+
homepage "https://github.com/osodevops/keito-cli"
102+
version "${VERSION}"
103+
license "MIT"
104+
105+
on_macos do
106+
if Hardware::CPU.arm?
107+
url "${BASE_URL}/keito-aarch64-apple-darwin.tar.gz"
108+
sha256 "${SHA_ARM64}"
109+
else
110+
url "${BASE_URL}/keito-x86_64-apple-darwin.tar.gz"
111+
sha256 "${SHA_X86_64_MAC}"
112+
end
113+
end
114+
115+
on_linux do
116+
url "${BASE_URL}/keito-x86_64-unknown-linux-gnu.tar.gz"
117+
sha256 "${SHA_X86_64_LINUX}"
118+
end
119+
120+
def install
121+
bin.install "keito"
122+
end
123+
124+
test do
125+
assert_match "keito-cli #{version}", shell_output("#{bin}/keito --version")
126+
end
127+
end
128+
FORMULA
129+
130+
# Remove heredoc indentation
131+
sed -i 's/^ //' keito.rb
132+
133+
# Push to homebrew-tap
134+
git clone "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/osodevops/homebrew-tap.git" tap
135+
cp keito.rb tap/Formula/keito.rb
136+
cd tap
137+
git config user.name "github-actions[bot]"
138+
git config user.email "github-actions[bot]@users.noreply.github.com"
139+
git add Formula/keito.rb
140+
git commit -m "keito ${VERSION}"
141+
git push
142+
143+
scoop:
144+
needs: upload-assets
145+
runs-on: ubuntu-latest
146+
steps:
147+
- name: Trigger Scoop bucket update
148+
uses: peter-evans/repository-dispatch@v3
149+
with:
150+
token: ${{ secrets.SCOOP_BUCKET_TOKEN }}
151+
repository: osodevops/scoop-bucket
152+
event-type: update-keito-manifest
153+
client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "tag": "${{ github.ref_name }}"}'

.github/workflows/test.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
env:
10+
CARGO_TERM_COLOR: always
11+
RUSTFLAGS: -D warnings
12+
13+
jobs:
14+
fmt:
15+
name: Format
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
- uses: dtolnay/rust-toolchain@stable
20+
with:
21+
components: rustfmt
22+
- run: cargo fmt --all -- --check
23+
24+
clippy:
25+
name: Clippy
26+
runs-on: ubuntu-latest
27+
steps:
28+
- uses: actions/checkout@v4
29+
- uses: dtolnay/rust-toolchain@stable
30+
with:
31+
components: clippy
32+
- uses: Swatinem/rust-cache@v2
33+
- run: cargo clippy --all-targets -- -D warnings
34+
35+
test:
36+
name: Test
37+
runs-on: ${{ matrix.os }}
38+
strategy:
39+
matrix:
40+
os: [ubuntu-latest, macos-latest, windows-latest]
41+
steps:
42+
- uses: actions/checkout@v4
43+
- uses: dtolnay/rust-toolchain@stable
44+
- uses: Swatinem/rust-cache@v2
45+
- run: cargo test --all-targets
46+
47+
audit:
48+
name: Security Audit
49+
runs-on: ubuntu-latest
50+
steps:
51+
- uses: actions/checkout@v4
52+
- uses: rustsec/audit-check@v2
53+
with:
54+
token: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/target
2+
Cargo.lock
3+
*.swp
4+
*.swo
5+
.DS_Store
6+
.env

CHANGELOG.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/).
7+
8+
## [Unreleased]
9+
10+
### Added
11+
12+
- `keito time stop --discard` — abandon a running timer without saving, deletes the time entry
13+
- Richer JSON error output with `suggestion` and `details` fields for agent-friendly recovery hints
14+
15+
## [0.1.0] - 2026-03-05
16+
17+
### Added
18+
19+
- `keito auth login` — interactive API key and workspace setup with OS keyring storage
20+
- `keito auth logout` — remove stored credentials from keychain
21+
- `keito auth status` — check authentication status and credential source
22+
- `keito auth whoami` — show current user identity and workspace info
23+
- `keito time start` — start a timer for a project and task
24+
- `keito time stop` — stop the currently running timer
25+
- `keito time log` — log a completed time entry with duration (decimal hours or HH:MM)
26+
- `keito time list` — list time entries with date, project, task, and pagination filters
27+
- `keito time running` — show currently running timer
28+
- `keito projects list` — list available projects in the workspace
29+
- `keito projects show` — show project details by name, code, or ID
30+
- `keito projects tasks` — list workspace-global tasks
31+
- Dual output mode: human-readable tables (TTY default) and JSON (piped default, or `--json`)
32+
- Case-insensitive name/code/ID resolution for projects and tasks
33+
- Exit codes 0-8 per specification (auth, input, conflict, not-found, rate-limit, server, network, config)
34+
- JSON error output with structured `{error, code, message}` format
35+
- Retry logic: 3x exponential backoff (1s, 2s, 4s) for network and server errors
36+
- Credential resolution: `KEITO_API_KEY` env > OS keyring > config file
37+
- Workspace resolution: `--workspace` flag > `KEITO_WORKSPACE_ID` env > config file
38+
- Configuration file at `~/.config/keito/config.toml`
39+
- Rich `--help` documentation with examples, exit codes, agent workflows, and env var reference
40+
- CI pipeline: format, clippy, multi-platform tests, security audit
41+
- Release pipeline: auto-tag on version bump, cross-platform builds, GitHub Releases, Homebrew tap, Scoop bucket

Cargo.toml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
[package]
2+
name = "keito-cli"
3+
version = "0.1.0"
4+
edition = "2021"
5+
description = "CLI for AI agents and humans to track billable time against the Keito platform"
6+
license = "MIT"
7+
repository = "https://github.com/osodevops/keito-cli"
8+
keywords = ["keito", "time-tracking", "cli", "ai-agents", "billing"]
9+
categories = ["command-line-utilities"]
10+
11+
[lib]
12+
name = "keito_cli"
13+
path = "src/lib.rs"
14+
15+
[[bin]]
16+
name = "keito"
17+
path = "src/main.rs"
18+
19+
[dependencies]
20+
clap = { version = "4", features = ["derive", "env"] }
21+
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
22+
tokio = { version = "1", features = ["full"] }
23+
serde = { version = "1", features = ["derive"] }
24+
serde_json = "1"
25+
toml = "0.8"
26+
tabled = "0.17"
27+
keyring = { version = "3", features = ["apple-native", "linux-native"] }
28+
chrono = { version = "0.4", features = ["serde"] }
29+
dirs = "6"
30+
thiserror = "2"
31+
colored = "3"
32+
dialoguer = "0.11"
33+
atty = "0.2"
34+
35+
[dev-dependencies]
36+
assert_cmd = "2"
37+
predicates = "3"
38+
wiremock = "0.6"
39+
tempfile = "3"
40+
41+
[profile.release]
42+
strip = true
43+
lto = true
44+
codegen-units = 1

dist-workspace.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[workspace]
2+
members = ["./"]
3+
4+
[dist]
5+
targets = [
6+
"aarch64-apple-darwin",
7+
"x86_64-apple-darwin",
8+
"x86_64-unknown-linux-gnu",
9+
"x86_64-pc-windows-msvc",
10+
]
11+
installers = ["shell", "powershell", "homebrew"]
12+
tap = "osodevops/homebrew-tap"
13+
publish-jobs = ["homebrew"]

0 commit comments

Comments
 (0)