diff --git a/.github/instructions/copilot.instructions.md b/.github/instructions/copilot.instructions.md index 0d440e5..21e3339 100644 --- a/.github/instructions/copilot.instructions.md +++ b/.github/instructions/copilot.instructions.md @@ -13,6 +13,8 @@ Reference: https://github.com/wgtechlabs/clean-commit ```text : (): + !: + ! (): ``` ## The 9 Types @@ -32,6 +34,7 @@ Reference: https://github.com/wgtechlabs/clean-commit ## Rules - Use lowercase for type +- Use `!` immediately after type (no space) to signal a breaking change โ€” only for `new`, `update`, `remove`, `security` - Use present tense ("add" not "added") - No period at the end - Keep description under 72 characters @@ -47,3 +50,5 @@ Reference: https://github.com/wgtechlabs/clean-commit - `๐Ÿงช test: add unit tests for auth service` - `๐Ÿ“– docs: update installation instructions` - `๐Ÿš€ release: version 1.0.0` +- `๐Ÿ“ฆ new!: completely redesign authentication system` +- `๐Ÿ”ง update! (api): change response format for all endpoints` diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml new file mode 100644 index 0000000..f288543 --- /dev/null +++ b/.github/workflows/commit-lint.yml @@ -0,0 +1,61 @@ +name: Commit Lint + +on: + pull_request: + branches: [main, dev] + push: + branches: [main, dev] + +permissions: + contents: read + pull-requests: write + +jobs: + lint-commits: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate commit messages + run: | + # Clean Commit convention pattern + # Format: [()]: + PATTERN='^(๐Ÿ“ฆ|๐Ÿ”ง|๐Ÿ—‘๏ธ|๐Ÿ”’|โš™๏ธ|โ˜•|๐Ÿงช|๐Ÿ“–|๐Ÿš€) (new|update|remove|security|setup|chore|test|docs|release)( \([a-z0-9][a-z0-9-]*\))?: .{1,72}$' + + if [ "${{ github.event_name }}" = "pull_request" ]; then + COMMITS=$(git log --format="%s" "${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}") + else + GITHUB_EVENT_BEFORE="${{ github.event.before }}" + GITHUB_EVENT_AFTER="${{ github.event.after }}" + if [ "$GITHUB_EVENT_BEFORE" = "0000000000000000000000000000000000000000" ]; then + # Initial push has no valid "before" SHA, so capture all reachable commits + COMMITS=$(git log --format="%s" "$GITHUB_EVENT_AFTER") + else + COMMITS=$(git log --format="%s" "${GITHUB_EVENT_BEFORE}..${GITHUB_EVENT_AFTER}") + fi + fi + + FAILED=0 + while IFS= read -r msg; do + [ -z "$msg" ] && continue + # Allow merge commits + if echo "$msg" | grep -qE "^Merge "; then + continue + fi + if ! echo "$msg" | grep -qP "$PATTERN"; then + echo "โœ– Invalid commit message: $msg" + FAILED=1 + else + echo "โœ” Valid commit message: $msg" + fi + done <<< "$COMMITS" + + if [ "$FAILED" -eq 1 ]; then + echo "" + echo "One or more commit messages do not follow the Clean Commit convention." + echo "Format: [()]: " + echo "Reference: https://github.com/wgtechlabs/clean-commit" + exit 1 + fi diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index d999b53..90e74ed 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -1,8 +1,14 @@ name: Container Build on: + pull_request: + branches: [main, dev] + push: + branches: [main, dev] workflow_call: workflow_dispatch: + release: + types: [published] permissions: contents: read diff --git a/.github/workflows/landing.yml b/.github/workflows/landing.yml new file mode 100644 index 0000000..d3fe727 --- /dev/null +++ b/.github/workflows/landing.yml @@ -0,0 +1,50 @@ +name: Deploy Landing Page + +on: + push: + branches: [main] + paths: + - 'src/landing/**' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2 + with: + bun-version: 1.2.x + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build landing page + run: bun run --cwd src/landing build + + - name: Upload artifact + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 + with: + path: src/landing/dist + + deploy: + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index bb337f2..01fbe1d 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -1,6 +1,10 @@ name: Package Build on: + pull_request: + branches: [main, dev] + push: + branches: [main, dev] workflow_call: workflow_dispatch: inputs: @@ -8,6 +12,8 @@ on: description: 'Perform dry run without publishing' type: boolean default: true + release: + types: [published] permissions: contents: read @@ -31,8 +37,11 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile + - name: Build workspace packages + run: bun run build:packages + - name: Build & Publish Packages - uses: wgtechlabs/package-build-flow-action@v2.0.0 + uses: wgtechlabs/package-build-flow-action@v2.0.1 with: monorepo: 'true' workspace-detection: 'true' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2dbd038..670c1ba 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,8 +8,6 @@ on: permissions: contents: write - packages: write - security-events: write pull-requests: write jobs: @@ -42,20 +40,8 @@ jobs: echo "changed=false" >> $GITHUB_OUTPUT fi - package: - needs: check-version - if: needs.check-version.outputs.changed == 'true' - uses: ./.github/workflows/package.yml - secrets: inherit - - container: - needs: check-version - if: needs.check-version.outputs.changed == 'true' - uses: ./.github/workflows/container.yml - secrets: inherit - release: - needs: [check-version, package, container] + needs: check-version if: needs.check-version.outputs.changed == 'true' runs-on: ubuntu-latest steps: @@ -64,9 +50,9 @@ jobs: fetch-depth: 0 - name: Create Release - uses: wgtechlabs/release-build-flow-action@032b32cb5f6933e5912768c5b2e42c29389c2c95 # v1.0.0 + uses: wgtechlabs/release-build-flow-action@799974c8dec094fbe94a92b2764365d3a8f9ce5f # v1.2.1 with: - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ secrets.GH_PAT }} monorepo: 'true' workspace-detection: 'true' package-manager: 'bun' diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..d03b211 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,3 @@ +#!/bin/sh + +bun "$(dirname "$0")/validate-commit-msg.mjs" "$1" diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..e69de29 diff --git a/.husky/validate-commit-msg.mjs b/.husky/validate-commit-msg.mjs new file mode 100644 index 0000000..71ad87b --- /dev/null +++ b/.husky/validate-commit-msg.mjs @@ -0,0 +1,69 @@ +import { readFileSync } from "fs"; + +const msgFile = process.argv[2]; +if (!msgFile) { + console.error("Error: No commit message file path provided."); + process.exit(1); +} +let raw; +try { + raw = readFileSync(msgFile, "utf8"); +} catch (err) { + console.error( + `Error: Could not read commit message file "${msgFile}": ${err.message}`, + ); + process.exit(1); +} +const firstLine = raw.replace(/\r/g, "").split("\n")[0].trim(); + +// Allow merge and revert commits +if (/^Merge /.test(firstLine) || /^Revert /.test(firstLine)) process.exit(0); + +// Clean Commit convention pattern +// Format: [!][()]: +const pattern = + /^(๐Ÿ“ฆ|๐Ÿ”ง|๐Ÿ—‘\uFE0F?|๐Ÿ”’|โš™\uFE0F?|โ˜•|๐Ÿงช|๐Ÿ“–|๐Ÿš€) (new|update|remove|security|setup|chore|test|docs|release)(!?)( \([a-z0-9][a-z0-9-]*\))?: .{1,72}$/u; + +// Only new, update, remove, security may use the breaking change marker +const breakingMatch = firstLine.match(pattern); +if (breakingMatch) { + const type = breakingMatch[2]; + const bang = breakingMatch[3]; + if (bang === '!' && !['new', 'update', 'remove', 'security'].includes(type)) { + console.error(''); + console.error('โœ– Breaking change marker (!) is only allowed for: new, update, remove, security'); + console.error(''); + process.exit(1); + } +} + +if (!pattern.test(firstLine)) { + console.error(""); + console.error("โœ– Invalid commit message format."); + console.error(""); + console.error(" Expected: [!][()]: "); + console.error(""); + console.error(" Use ! after type for breaking changes (new, update, remove, security only)"); + console.error(""); + console.error(" Types and emojis:"); + console.error(" ๐Ÿ“ฆ new โ€“ new features, files, or capabilities"); + console.error(" ๐Ÿ”ง update โ€“ changes, refactoring, improvements"); + console.error(" ๐Ÿ—‘๏ธ remove โ€“ removing code, files, or dependencies"); + console.error(" ๐Ÿ”’ security โ€“ security fixes or patches"); + console.error(" โš™๏ธ setup โ€“ configs, CI/CD, tooling, build systems"); + console.error(" โ˜• chore โ€“ maintenance, dependency updates"); + console.error(" ๐Ÿงช test โ€“ adding or updating tests"); + console.error(" ๐Ÿ“– docs โ€“ documentation changes"); + console.error(" ๐Ÿš€ release โ€“ version releases"); + console.error(""); + console.error(" Examples:"); + console.error(" ๐Ÿ“ฆ new: user authentication system"); + console.error(" ๐Ÿ”ง update (api): improve error handling"); + console.error(" โš™๏ธ setup (ci): configure github actions workflow"); + console.error(" ๐Ÿ“ฆ new!: completely redesign authentication system"); + console.error(" ๐Ÿ”ง update! (api): change response format for all endpoints"); + console.error(""); + console.error(" Reference: https://github.com/wgtechlabs/clean-commit"); + console.error(""); + process.exit(1); +} diff --git a/AGENTS.md b/AGENTS.md index 4206fe8..717a06d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,8 @@ Reference: https://github.com/wgtechlabs/clean-commit ```text : (): + !: + ! (): ``` ## The 9 Types @@ -28,6 +30,7 @@ Reference: https://github.com/wgtechlabs/clean-commit ## Rules - Use lowercase for type +- Use `!` immediately after type (no space) to signal a breaking change โ€” only for `new`, `update`, `remove`, `security` - Use present tense ("add" not "added") - No period at the end - Keep description under 72 characters @@ -43,3 +46,5 @@ Reference: https://github.com/wgtechlabs/clean-commit - `๐Ÿงช test: add unit tests for auth service` - `๐Ÿ“– docs: update installation instructions` - `๐Ÿš€ release: version 1.0.0` +- `๐Ÿ“ฆ new!: completely redesign authentication system` +- `๐Ÿ”ง update! (api): change response format for all endpoints` diff --git a/Dockerfile b/Dockerfile index 84f9869..8acfc2f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # โ”€โ”€ Stage 1: Install + Build โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -FROM oven/bun:1 AS builder +FROM oven/bun:1.3.9 AS builder WORKDIR /app @@ -31,7 +31,7 @@ COPY plugins/channel/plugin-channel-friends/package.json ./plugins/channel/plugi COPY plugins/provider/plugin-provider-openai/package.json ./plugins/provider/plugin-provider-openai/ # Install all deps (dev included โ€” needed to build) -RUN bun install --frozen-lockfile +RUN bun install # Copy source COPY . . @@ -40,7 +40,7 @@ COPY . . RUN bun run build # โ”€โ”€ Stage 2: Production โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -FROM oven/bun:1-slim AS production +FROM oven/bun:1.3.9-slim AS production WORKDIR /app diff --git a/README.md b/README.md index b1b2708..b8ab0e0 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Tiny Claw is inspired by personal AI companions from science fiction like **Cods - **Self-configuring.** No manual config files. The agent configures itself through conversation. - **Own personality.** Ships with a personality (Heartware system) that's uniquely its own. - **Native, not wrapped.** Every component is built from scratch with zero dependency on external AI frameworks. -- **Easy to start.** Uses Ollama Cloud with two built-in models โ€” kimi-k2.5 (default) and gpt-oss:120b. Choose your model during setup and switch anytime via conversation. +- **Easy to start.** Uses Ollama Cloud with two built-in models โ€” kimi-k2.5:cloud (default) and gpt-oss:120b-cloud. Choose your model during setup and switch anytime via conversation. - **Cost-conscious.** Smart routing tiers queries across your installed providers. Cheap models handle simple stuff, powerful models only fire when needed. ## โœจ Features @@ -120,6 +120,7 @@ tinyclaw/ router/ Smart provider routing (8-dim classifier) learning/ Behavioral pattern detection sandbox/ Bun Worker code execution + shell/ Controlled shell execution with permission engine shield/ Runtime SHIELD.md enforcement + anti-malware pulse/ Cron-like proactive scheduler queue/ Per-session message locking queue @@ -129,11 +130,11 @@ tinyclaw/ secrets/ Encrypted secrets management (AES-256-GCM) plugins/ Plugin discovery and loading plugins/ Plugin packages (keep the core tiny) - channel/ Messaging integrations (Discord, etc.) + channel/ Messaging integrations (Discord, Friends, etc.) provider/ LLM providers (OpenAI, etc.) - tool/ Additional agent tools src/ cli/ CLI entry point + landing/ Official landing page (Svelte + Vite) web/ Web UI (Svelte 5, Discord-like experience) ``` @@ -157,7 +158,7 @@ Read the project's [code of conduct](https://github.com/warengonzaga/tinyclaw/bl ## ๐Ÿ“ƒ License -This project is licensed under [MIT License](https://opensource.org/license/MIT). +This project is licensed under [GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0.html). ## ๐Ÿ™ Credits diff --git a/bun.lock b/bun.lock index ec7912a..6219459 100644 --- a/bun.lock +++ b/bun.lock @@ -10,12 +10,13 @@ "devDependencies": { "@types/bun": "latest", "@types/node": "^22.10.0", + "husky": "^9.1.7", "typescript": "^5.7.0", }, }, "packages/compactor": { "name": "@tinyclaw/compactor", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@tinyclaw/logger": "workspace:*", "@tinyclaw/types": "workspace:*", @@ -27,7 +28,7 @@ }, "packages/config": { "name": "@tinyclaw/config", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@tinyclaw/core": "workspace:*", "@tinyclaw/logger": "workspace:*", @@ -38,7 +39,7 @@ }, "packages/core": { "name": "@tinyclaw/core", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@tinyclaw/delegation": "workspace:*", "@tinyclaw/logger": "workspace:*", @@ -50,7 +51,7 @@ }, "packages/delegation": { "name": "@tinyclaw/delegation", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@tinyclaw/logger": "workspace:*", "@tinyclaw/router": "workspace:*", @@ -64,7 +65,7 @@ }, "packages/heartware": { "name": "@tinyclaw/heartware", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@tinyclaw/logger": "workspace:*", "@tinyclaw/types": "workspace:*", @@ -72,29 +73,29 @@ }, "packages/intercom": { "name": "@tinyclaw/intercom", - "version": "1.0.0", + "version": "1.0.1", }, "packages/learning": { "name": "@tinyclaw/learning", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@tinyclaw/types": "workspace:*", }, }, "packages/logger": { "name": "@tinyclaw/logger", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@wgtechlabs/log-engine": "^2.3.0", }, }, "packages/matcher": { "name": "@tinyclaw/matcher", - "version": "1.0.0", + "version": "1.0.1", }, "packages/memory": { "name": "@tinyclaw/memory", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@tinyclaw/types": "workspace:*", }, @@ -104,7 +105,7 @@ }, "packages/plugins": { "name": "@tinyclaw/plugins", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@tinyclaw/logger": "workspace:*", "@tinyclaw/types": "workspace:*", @@ -112,7 +113,7 @@ }, "packages/pulse": { "name": "@tinyclaw/pulse", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@tinyclaw/logger": "workspace:*", "@tinyclaw/types": "workspace:*", @@ -120,11 +121,11 @@ }, "packages/queue": { "name": "@tinyclaw/queue", - "version": "1.0.0", + "version": "1.0.1", }, "packages/router": { "name": "@tinyclaw/router", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@tinyclaw/logger": "workspace:*", "@tinyclaw/types": "workspace:*", @@ -132,11 +133,11 @@ }, "packages/sandbox": { "name": "@tinyclaw/sandbox", - "version": "1.0.0", + "version": "1.0.1", }, "packages/secrets": { "name": "@tinyclaw/secrets", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@tinyclaw/logger": "workspace:*", "@tinyclaw/types": "workspace:*", @@ -145,7 +146,7 @@ }, "packages/shell": { "name": "@tinyclaw/shell", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@tinyclaw/logger": "workspace:*", "@tinyclaw/types": "workspace:*", @@ -153,7 +154,7 @@ }, "packages/shield": { "name": "@tinyclaw/shield", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@tinyclaw/logger": "workspace:*", "@tinyclaw/types": "workspace:*", @@ -161,11 +162,11 @@ }, "packages/types": { "name": "@tinyclaw/types", - "version": "1.0.0", + "version": "1.0.1", }, "plugins/channel/plugin-channel-discord": { "name": "@tinyclaw/plugin-channel-discord", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@tinyclaw/logger": "workspace:*", "@tinyclaw/types": "workspace:*", @@ -174,7 +175,7 @@ }, "plugins/channel/plugin-channel-friends": { "name": "@tinyclaw/plugin-channel-friends", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@tinyclaw/logger": "workspace:*", "@tinyclaw/types": "workspace:*", @@ -182,7 +183,7 @@ }, "plugins/provider/plugin-provider-openai": { "name": "@tinyclaw/plugin-provider-openai", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@tinyclaw/logger": "workspace:*", "@tinyclaw/types": "workspace:*", @@ -190,7 +191,7 @@ }, "src/cli": { "name": "tinyclaw", - "version": "1.0.0", + "version": "1.0.1", "bin": { "tinyclaw": "./dist/index.js", }, @@ -226,9 +227,22 @@ "@types/qrcode": "^1.5.6", }, }, + "src/landing": { + "name": "@tinyclaw/landing", + "version": "1.0.1", + "dependencies": { + "svelte": "^5.20.1", + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.1.4", + "@tailwindcss/vite": "^4.1.18", + "tailwindcss": "^4.1.18", + "vite": "^7.2.4", + }, + }, "src/web": { "name": "@tinyclaw/web", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@tinyclaw/core": "workspace:*", "@tinyclaw/heartware": "workspace:*", @@ -433,6 +447,8 @@ "@tinyclaw/intercom": ["@tinyclaw/intercom@workspace:packages/intercom"], + "@tinyclaw/landing": ["@tinyclaw/landing@workspace:src/landing"], + "@tinyclaw/learning": ["@tinyclaw/learning@workspace:packages/learning"], "@tinyclaw/logger": ["@tinyclaw/logger@workspace:packages/logger"], @@ -553,6 +569,8 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], diff --git a/package.json b/package.json index 5a15840..a945800 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tinyclaw-monorepo", - "version": "1.0.0", + "version": "1.1.0", "description": "Your autonomous AI companion", "license": "GPL-3.0", "author": "Waren Gonzaga", @@ -19,15 +19,19 @@ "build:plugins": "bun run --filter './plugins/**/*' build", "build:apps": "bun run --cwd src/web build && bun run --cwd src/cli build", "build:ui": "bun run --cwd src/web build", + "build:landing": "bun run --cwd src/landing build", "build:all": "bun run build:packages && bun run build:plugins && bun run build:apps", "start": "bun run --cwd src/cli start", "test": "bun test", "cli": "bun run src/cli/src/index.ts", - "dev:purge": "bun run cli purge --force" + "dev:purge": "bun run cli purge --force", + "dev:landing": "bun run --cwd src/landing dev", + "prepare": "husky" }, "devDependencies": { "@types/bun": "latest", "@types/node": "^22.10.0", + "husky": "^9.1.7", "typescript": "^5.7.0" }, "dependencies": { diff --git a/packages/compactor/package.json b/packages/compactor/package.json index cc81485..229f50e 100644 --- a/packages/compactor/package.json +++ b/packages/compactor/package.json @@ -1,6 +1,7 @@ { "name": "@tinyclaw/compactor", - "version": "1.0.0", + "private": true, + "version": "1.1.0", "description": "Layered conversation compaction and token optimization for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/packages/config/package.json b/packages/config/package.json index e2ea3b3..8696a3a 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,7 @@ { "name": "@tinyclaw/config", - "version": "1.0.0", + "private": true, + "version": "1.1.0", "description": "SQLite-backed persistent configuration for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/packages/core/package.json b/packages/core/package.json index ddc8287..b630362 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,7 @@ { "name": "@tinyclaw/core", - "version": "1.0.0", + "private": true, + "version": "1.1.0", "description": "Core agent runtime for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b63cf5b..3d20616 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -49,3 +49,12 @@ export { BACKUP_CODE_LENGTH, RECOVERY_TOKEN_LENGTH, } from './owner-auth.js'; + +// Update checker โ€” npm registry polling + system prompt context +export { + checkForUpdate, + buildUpdateContext, + detectRuntime, + isNewerVersion, +} from './update-checker.js'; +export type { UpdateInfo, UpdateRuntime } from './update-checker.js'; diff --git a/packages/core/src/loop.ts b/packages/core/src/loop.ts index 841387c..c02ee98 100644 --- a/packages/core/src/loop.ts +++ b/packages/core/src/loop.ts @@ -760,6 +760,11 @@ export async function agentLoop( } } + // Inject software update context (if an update is available) + if (context.updateContext) { + basePrompt += context.updateContext; + } + const systemPrompt = learning.injectIntoPrompt(basePrompt, learnedContext); // Sanitize user message for prompt injection defense (friends only) diff --git a/packages/core/src/update-checker.ts b/packages/core/src/update-checker.ts new file mode 100644 index 0000000..6ecc7ce --- /dev/null +++ b/packages/core/src/update-checker.ts @@ -0,0 +1,303 @@ +/** + * Update Checker + * + * Lightweight, non-blocking module that checks the npm registry for newer + * versions of tinyclaw. Results are cached locally (24-hour TTL) to avoid + * repeated network calls. + * + * The update info is injected into the agent's system prompt context so the + * AI can conversationally inform the user about available upgrades. + * + * Runtime detection differentiates npm installs (self-upgradable via shell + * tool) from Docker containers (manual pull required). + */ + +import { join } from 'node:path'; +import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; +import { logger } from '@tinyclaw/logger'; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export type UpdateRuntime = 'npm' | 'docker' | 'source'; + +export interface UpdateInfo { + /** Currently running version (e.g. "1.0.0"). */ + current: string; + /** Latest version published on npm (e.g. "1.1.0"). */ + latest: string; + /** Whether a newer version is available. */ + updateAvailable: boolean; + /** Detected runtime environment. */ + runtime: UpdateRuntime; + /** Timestamp (ms) of the last check. */ + checkedAt: number; + /** GitHub release URL for the latest version. */ + releaseUrl: string; +} + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +/** Time-to-live for the cache file (24 hours). */ +const CACHE_TTL_MS = 24 * 60 * 60 * 1000; + +/** npm registry endpoint for the tinyclaw package. */ +const NPM_REGISTRY_URL = 'https://registry.npmjs.org/tinyclaw/latest'; + +/** Maximum time to wait for the registry response (ms). */ +const FETCH_TIMEOUT_MS = 5_000; + +/** Cache file name within the data directory. */ +const CACHE_FILENAME = 'update-check.json'; + +/** GitHub releases base URL. */ +const GITHUB_RELEASES_URL = 'https://github.com/warengonzaga/tinyclaw/releases/tag'; + +// --------------------------------------------------------------------------- +// Runtime detection +// --------------------------------------------------------------------------- + +/** + * Detect the runtime environment. + * + * - Docker: `/.dockerenv` exists or `TINYCLAW_RUNTIME` env is set to "docker" + * - Source: `TINYCLAW_RUNTIME` env is set to "source" + * - npm: everything else (global install via npm/bun/pnpm) + */ +export function detectRuntime(): UpdateRuntime { + const envRuntime = process.env.TINYCLAW_RUNTIME?.toLowerCase(); + if (envRuntime === 'docker') return 'docker'; + if (envRuntime === 'source') return 'source'; + + // Docker container detection + try { + if (existsSync('/.dockerenv')) return 'docker'; + } catch { + // Permission errors on exotic platforms โ€” assume npm + } + + return 'npm'; +} + +// --------------------------------------------------------------------------- +// Semver comparison (minimal โ€” avoids pulling a full semver library) +// --------------------------------------------------------------------------- + +/** Matches a semver-like version string (with optional v-prefix). */ +const SEMVER_RE = /^v?\d+\.\d+\.\d+/; + +/** Matches a safe HTTPS URL. */ +const SAFE_URL_RE = /^https?:\/\/[^\s]+$/; + +/** + * Sanitize a string for safe interpolation into a system prompt. + * Validates against an expected pattern and strips characters that could + * be used for prompt injection (newlines, backticks, markdown markers). + */ +export function sanitizeForPrompt( + value: string, + kind: 'version' | 'url', +): string { + const trimmed = value.trim(); + if (kind === 'version') { + if (!SEMVER_RE.test(trimmed)) return 'unknown'; + // Strip everything after the patch number to remove injected text + return trimmed.replace(/^(v?\d+\.\d+\.\d+)[\s\S]*$/, '$1'); + } + // kind === 'url' + if (!SAFE_URL_RE.test(trimmed)) return '(unavailable)'; + // Remove characters that could break prompt formatting + return trimmed.replace(/[`\n\r\[\](){}#*_~>|]/g, ''); +} + +/** + * Compare two semver strings. Returns true when `latest` is strictly newer + * than `current`. Only handles `MAJOR.MINOR.PATCH`; pre-release suffixes + * are ignored. + */ +export function isNewerVersion(current: string, latest: string): boolean { + const parse = (v: string): number[] => + v + .replace(/^v/, '') + .replace(/[-+].*$/, '') + .split('.') + .map((s) => { const n = Number(s); return isNaN(n) ? 0 : n; }) + .slice(0, 3); + const [cMaj = 0, cMin = 0, cPat = 0] = parse(current); + const [lMaj = 0, lMin = 0, lPat = 0] = parse(latest); + if (lMaj !== cMaj) return lMaj > cMaj; + if (lMin !== cMin) return lMin > cMin; + return lPat > cPat; +} + +// --------------------------------------------------------------------------- +// Cache I/O +// --------------------------------------------------------------------------- + +function getCachePath(dataDir: string): string { + return join(dataDir, 'data', CACHE_FILENAME); +} + +function readCache(dataDir: string): UpdateInfo | null { + try { + const raw = readFileSync(getCachePath(dataDir), 'utf-8'); + const cached = JSON.parse(raw) as UpdateInfo; + const validRuntimes: UpdateRuntime[] = ['npm', 'docker', 'source']; + if (cached && typeof cached.checkedAt === 'number' && typeof cached.latest === 'string' && validRuntimes.includes(cached.runtime as UpdateRuntime)) return cached; + } catch { + // Missing or corrupt โ€” will re-check + } + return null; +} + +function writeCache(dataDir: string, info: UpdateInfo): void { + try { + const dir = join(dataDir, 'data'); + mkdirSync(dir, { recursive: true }); + writeFileSync(getCachePath(dataDir), JSON.stringify(info, null, 2), 'utf-8'); + } catch (err) { + logger.debug('Failed to write update cache', err); + } +} + +// --------------------------------------------------------------------------- +// Registry fetch +// --------------------------------------------------------------------------- + +async function fetchLatestVersion(): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + const res = await fetch(NPM_REGISTRY_URL, { + signal: controller.signal, + headers: { Accept: 'application/json' }, + }); + + if (!res.ok) return null; + const data = (await res.json()) as { version?: string }; + return data.version ?? null; + } catch { + // Network error, timeout, or offline โ€” silently return null + return null; + } finally { + clearTimeout(timeout); + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** + * Check for available updates. + * + * - Returns cached result if the cache is still fresh (< 24 hours old). + * - Otherwise fetches the npm registry in the background. + * - Never throws โ€” returns null on any failure so startup is never delayed. + * + * @param currentVersion - The currently running version string. + * @param dataDir - The tinyclaw data directory (e.g. `~/.tinyclaw`). + */ +export async function checkForUpdate( + currentVersion: string, + dataDir: string, +): Promise { + try { + // Return cached result if still fresh + const cached = readCache(dataDir); + if (cached && Date.now() - cached.checkedAt < CACHE_TTL_MS) { + // Re-evaluate against the current binary version (in case user + // upgraded manually since the last check) + return { + ...cached, + current: currentVersion, + updateAvailable: isNewerVersion(currentVersion, cached.latest), + }; + } + + // Fetch latest version from npm + const latest = await fetchLatestVersion(); + if (!latest) { + // Network failure โ€” use stale cache if available, but recompute + // against the caller-supplied currentVersion + if (cached) { + return { + ...cached, + current: currentVersion, + updateAvailable: isNewerVersion(currentVersion, cached.latest), + }; + } + return null; + } + + const runtime = detectRuntime(); + const info: UpdateInfo = { + current: currentVersion, + latest, + updateAvailable: isNewerVersion(currentVersion, latest), + runtime, + checkedAt: Date.now(), + releaseUrl: `${GITHUB_RELEASES_URL}/v${latest}`, + }; + + writeCache(dataDir, info); + + if (info.updateAvailable) { + logger.info('Update available', { current: currentVersion, latest, runtime }, { emoji: '๐Ÿ†•' }); + } + + return info; + } catch (err) { + logger.debug('Update check failed', err); + return null; + } +} + +// --------------------------------------------------------------------------- +// System prompt context builder +// --------------------------------------------------------------------------- + +/** + * Build a system prompt section that informs the agent about an available + * update. Returns an empty string if no update is available or info is null. + */ +export function buildUpdateContext(info: UpdateInfo | null): string { + if (!info?.updateAvailable) return ''; + + const safeCurrent = sanitizeForPrompt(info.current, 'version'); + const safeLatest = sanitizeForPrompt(info.latest, 'version'); + const safeReleaseUrl = sanitizeForPrompt(info.releaseUrl, 'url'); + + const upgradeInstructions = + info.runtime === 'npm' + ? `Since you are running as an npm global install, you can upgrade yourself using the shell tool: +\`bun install -g tinyclaw@latest\` or \`npm install -g tinyclaw@latest\` +After the upgrade completes, request a restart using the tinyclaw_restart tool so the supervisor can reload with the new version.` + : info.runtime === 'docker' + ? `Since you are running inside a Docker container, you cannot upgrade yourself directly. +Instruct the owner to run the following commands externally: +\`docker pull ghcr.io/warengonzaga/tinyclaw:latest\` +Then restart the container (e.g. \`docker restart tinyclaw\` or \`docker compose up -d\`).` + : `Since you are running from source, instruct the owner to pull the latest changes and rebuild: +\`git pull && bun install && bun run build:packages\` +Then restart using the tinyclaw_restart tool.`; + + return ` + +## Software Update Available +- **Current version:** ${safeCurrent} +- **Latest version:** ${safeLatest} +- **Runtime:** ${info.runtime} +- **Release notes:** ${safeReleaseUrl} + +${upgradeInstructions} + +**Behavior guidelines:** +- Mention the update naturally early in the conversation, but do not interrupt an ongoing task. +- Do not repeat the update reminder if the owner has already acknowledged or dismissed it. +- If the owner agrees to update, proceed with the appropriate upgrade path above. +- After a successful upgrade and restart, confirm the new version is running.`; +} diff --git a/packages/core/tests/update-checker.test.ts b/packages/core/tests/update-checker.test.ts new file mode 100644 index 0000000..1b4a909 --- /dev/null +++ b/packages/core/tests/update-checker.test.ts @@ -0,0 +1,295 @@ +/** + * Tests for the update checker module. + * + * Validates version comparison, runtime detection, system prompt context + * building, cache I/O, and the main checkForUpdate flow. + */ + +import { describe, expect, test, beforeEach, afterEach } from 'bun:test'; +import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; +import { + isNewerVersion, + detectRuntime, + buildUpdateContext, + checkForUpdate, + sanitizeForPrompt, + type UpdateInfo, +} from '../src/update-checker.js'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function createTempDir(suffix: string): string { + const dir = join(tmpdir(), `tinyclaw-update-test-${suffix}-${Date.now()}`); + mkdirSync(join(dir, 'data'), { recursive: true }); + return dir; +} + +function makeMockInfo(overrides: Partial = {}): UpdateInfo { + return { + current: '1.0.0', + latest: '1.1.0', + updateAvailable: true, + runtime: 'npm', + checkedAt: Date.now(), + releaseUrl: 'https://github.com/warengonzaga/tinyclaw/releases/tag/v1.1.0', + ...overrides, + }; +} + +// --------------------------------------------------------------------------- +// isNewerVersion +// --------------------------------------------------------------------------- + +describe('isNewerVersion', () => { + test('detects major bump', () => { + expect(isNewerVersion('1.0.0', '2.0.0')).toBe(true); + }); + + test('detects minor bump', () => { + expect(isNewerVersion('1.0.0', '1.1.0')).toBe(true); + }); + + test('detects patch bump', () => { + expect(isNewerVersion('1.0.0', '1.0.1')).toBe(true); + }); + + test('returns false for same version', () => { + expect(isNewerVersion('1.0.0', '1.0.0')).toBe(false); + }); + + test('returns false for older version', () => { + expect(isNewerVersion('2.0.0', '1.9.9')).toBe(false); + }); + + test('handles v-prefix', () => { + expect(isNewerVersion('v1.0.0', 'v1.0.1')).toBe(true); + }); + + test('handles mixed v-prefix', () => { + expect(isNewerVersion('1.0.0', 'v2.0.0')).toBe(true); + }); + + test('handles double-digit versions', () => { + expect(isNewerVersion('1.9.0', '1.10.0')).toBe(true); + }); +}); + +// --------------------------------------------------------------------------- +// detectRuntime +// --------------------------------------------------------------------------- + +describe('detectRuntime', () => { + const originalEnv = process.env.TINYCLAW_RUNTIME; + + afterEach(() => { + if (originalEnv === undefined) { + delete process.env.TINYCLAW_RUNTIME; + } else { + process.env.TINYCLAW_RUNTIME = originalEnv; + } + }); + + test('returns docker when TINYCLAW_RUNTIME=docker', () => { + process.env.TINYCLAW_RUNTIME = 'docker'; + expect(detectRuntime()).toBe('docker'); + }); + + test('returns source when TINYCLAW_RUNTIME=source', () => { + process.env.TINYCLAW_RUNTIME = 'source'; + expect(detectRuntime()).toBe('source'); + }); + + test('returns npm by default', () => { + delete process.env.TINYCLAW_RUNTIME; + // On a normal dev machine without /.dockerenv, should return npm + const result = detectRuntime(); + expect(result === 'npm' || result === 'docker').toBe(true); + }); + + test('is case-insensitive for env var', () => { + process.env.TINYCLAW_RUNTIME = 'Docker'; + expect(detectRuntime()).toBe('docker'); + }); +}); + +// --------------------------------------------------------------------------- +// buildUpdateContext +// --------------------------------------------------------------------------- + +describe('buildUpdateContext', () => { + test('returns empty string when no update available', () => { + const info = makeMockInfo({ updateAvailable: false }); + expect(buildUpdateContext(info)).toBe(''); + }); + + test('returns empty string for null info', () => { + expect(buildUpdateContext(null)).toBe(''); + }); + + test('includes version info for npm runtime', () => { + const info = makeMockInfo({ runtime: 'npm' }); + const ctx = buildUpdateContext(info); + expect(ctx).toContain('1.0.0'); + expect(ctx).toContain('1.1.0'); + expect(ctx).toContain('bun install -g tinyclaw@latest'); + expect(ctx).toContain('tinyclaw_restart'); + }); + + test('includes docker pull instructions for docker runtime', () => { + const info = makeMockInfo({ runtime: 'docker' }); + const ctx = buildUpdateContext(info); + expect(ctx).toContain('docker pull'); + expect(ctx).toContain('ghcr.io/warengonzaga/tinyclaw'); + expect(ctx).toContain('cannot upgrade yourself directly'); + }); + + test('includes git pull instructions for source runtime', () => { + const info = makeMockInfo({ runtime: 'source' }); + const ctx = buildUpdateContext(info); + expect(ctx).toContain('git pull'); + expect(ctx).toContain('bun run build:packages'); + }); + + test('includes release URL', () => { + const info = makeMockInfo(); + const ctx = buildUpdateContext(info); + expect(ctx).toContain('releases/tag/v1.1.0'); + }); + + test('includes behavior guidelines', () => { + const info = makeMockInfo(); + const ctx = buildUpdateContext(info); + expect(ctx).toContain('do not interrupt'); + expect(ctx).toContain('Do not repeat'); + }); +}); + +// --------------------------------------------------------------------------- +// checkForUpdate โ€” cache behavior +// --------------------------------------------------------------------------- + +describe('checkForUpdate', () => { + let tempDir: string; + const originalFetch = globalThis.fetch; + + beforeEach(() => { + tempDir = createTempDir('check'); + // Default mock: simulate network failure so tests are deterministic + globalThis.fetch = (() => Promise.reject(new Error('mock network failure'))) as typeof fetch; + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + try { + rmSync(tempDir, { recursive: true, force: true }); + } catch { + // cleanup best-effort + } + }); + + test('returns cached result if cache is fresh', async () => { + const cached = makeMockInfo({ checkedAt: Date.now() }); + writeFileSync( + join(tempDir, 'data', 'update-check.json'), + JSON.stringify(cached), + ); + + const result = await checkForUpdate('1.0.0', tempDir); + expect(result).not.toBeNull(); + expect(result!.latest).toBe('1.1.0'); + expect(result!.updateAvailable).toBe(true); + }); + + test('re-evaluates updateAvailable against current version', async () => { + // Cache says latest=1.1.0, but we're now running 1.1.0 + const cached = makeMockInfo({ checkedAt: Date.now(), latest: '1.1.0' }); + writeFileSync( + join(tempDir, 'data', 'update-check.json'), + JSON.stringify(cached), + ); + + const result = await checkForUpdate('1.1.0', tempDir); + expect(result).not.toBeNull(); + expect(result!.updateAvailable).toBe(false); + expect(result!.current).toBe('1.1.0'); + }); + + test('returns null on network failure with no cache', async () => { + // No cache file, fetch mock rejects โ€” should return null + const result = await checkForUpdate('1.0.0', tempDir); + expect(result).toBeNull(); + }); + + test('handles corrupt cache file gracefully', async () => { + writeFileSync(join(tempDir, 'data', 'update-check.json'), 'not json!!!'); + + // Corrupt cache is unreadable and fetch mock rejects โ€” should return null + const result = await checkForUpdate('1.0.0', tempDir); + expect(result).toBeNull(); + }); + + test('creates data dir if missing', async () => { + const freshDir = join(tmpdir(), `tinyclaw-update-nodatadir-${Date.now()}`); + mkdirSync(freshDir, { recursive: true }); + + // Mock a successful fetch so cache gets written + globalThis.fetch = (() => + Promise.resolve( + new Response(JSON.stringify({ version: '1.1.0' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + )) as typeof fetch; + + try { + const result = await checkForUpdate('1.0.0', freshDir); + expect(result).not.toBeNull(); + expect(existsSync(join(freshDir, 'data', 'update-check.json'))).toBe(true); + } finally { + rmSync(freshDir, { recursive: true, force: true }); + } + }); +}); + +// --------------------------------------------------------------------------- +// sanitizeForPrompt +// --------------------------------------------------------------------------- + +describe('sanitizeForPrompt', () => { + test('accepts valid semver version', () => { + expect(sanitizeForPrompt('1.2.3', 'version')).toBe('1.2.3'); + }); + + test('accepts v-prefixed version', () => { + expect(sanitizeForPrompt('v1.2.3', 'version')).toBe('v1.2.3'); + }); + + test('strips trailing garbage from version', () => { + expect(sanitizeForPrompt('1.2.3-evil\nprompt', 'version')).toBe('1.2.3'); + }); + + test('returns unknown for non-semver version', () => { + expect(sanitizeForPrompt('not-a-version', 'version')).toBe('unknown'); + }); + + test('accepts valid https URL', () => { + const url = 'https://github.com/warengonzaga/tinyclaw/releases/tag/v1.0.0'; + expect(sanitizeForPrompt(url, 'url')).toBe( + 'https://github.com/warengonzaga/tinyclaw/releases/tag/v1.0.0', + ); + }); + + test('returns unavailable for non-http URL', () => { + expect(sanitizeForPrompt('javascript:alert(1)', 'url')).toBe('(unavailable)'); + }); + + test('strips markdown/injection characters from URL', () => { + const url = 'https://example.com/path`injection`'; + const result = sanitizeForPrompt(url, 'url'); + expect(result).not.toContain('`'); + }); +}); diff --git a/packages/delegation/package.json b/packages/delegation/package.json index 49d0f2e..bfcafaf 100644 --- a/packages/delegation/package.json +++ b/packages/delegation/package.json @@ -1,6 +1,7 @@ { "name": "@tinyclaw/delegation", - "version": "1.0.0", + "private": true, + "version": "1.1.0", "description": "Sub-agent delegation and lifecycle management for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/packages/heartware/package.json b/packages/heartware/package.json index b10b7ec..a51ac8f 100644 --- a/packages/heartware/package.json +++ b/packages/heartware/package.json @@ -1,6 +1,7 @@ { "name": "@tinyclaw/heartware", - "version": "1.0.0", + "private": true, + "version": "1.1.0", "description": "Personality and self-configuration system for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/packages/intercom/package.json b/packages/intercom/package.json index fc8173f..a670da3 100644 --- a/packages/intercom/package.json +++ b/packages/intercom/package.json @@ -1,6 +1,7 @@ { "name": "@tinyclaw/intercom", - "version": "1.0.0", + "private": true, + "version": "1.1.0", "description": "Lightweight pub/sub event bus for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/packages/learning/package.json b/packages/learning/package.json index a35e9a8..80c247f 100644 --- a/packages/learning/package.json +++ b/packages/learning/package.json @@ -1,6 +1,7 @@ { "name": "@tinyclaw/learning", - "version": "1.0.0", + "private": true, + "version": "1.1.0", "description": "Pattern detection and adaptive learning engine for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/packages/logger/package.json b/packages/logger/package.json index 2f37b4d..37ffc26 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,7 @@ { "name": "@tinyclaw/logger", - "version": "1.0.0", + "private": true, + "version": "1.1.0", "description": "Context-aware structured logging for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/packages/matcher/package.json b/packages/matcher/package.json index 401cb9a..8b16a69 100644 --- a/packages/matcher/package.json +++ b/packages/matcher/package.json @@ -1,6 +1,7 @@ { "name": "@tinyclaw/matcher", - "version": "1.0.0", + "private": true, + "version": "1.1.0", "description": "Hybrid semantic text matching engine for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/packages/memory/package.json b/packages/memory/package.json index f84e079..9419bf8 100644 --- a/packages/memory/package.json +++ b/packages/memory/package.json @@ -1,6 +1,7 @@ { "name": "@tinyclaw/memory", - "version": "1.0.0", + "private": true, + "version": "1.1.0", "description": "3-layer adaptive memory with temporal decay for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/packages/plugins/package.json b/packages/plugins/package.json index 1b586ae..eb92615 100644 --- a/packages/plugins/package.json +++ b/packages/plugins/package.json @@ -1,6 +1,6 @@ { "name": "@tinyclaw/plugins", - "version": "1.0.0", + "version": "1.1.0", "description": "Config-driven plugin loader and validator for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/packages/pulse/package.json b/packages/pulse/package.json index 7a7db1a..3b91ec4 100644 --- a/packages/pulse/package.json +++ b/packages/pulse/package.json @@ -1,6 +1,7 @@ { "name": "@tinyclaw/pulse", - "version": "1.0.0", + "private": true, + "version": "1.1.0", "description": "Cron-like recurring task scheduler for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/packages/queue/package.json b/packages/queue/package.json index 231660c..81c3ca6 100644 --- a/packages/queue/package.json +++ b/packages/queue/package.json @@ -1,6 +1,7 @@ { "name": "@tinyclaw/queue", - "version": "1.0.0", + "private": true, + "version": "1.1.0", "description": "Per-session promise-chain task serialization for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/packages/router/package.json b/packages/router/package.json index 3988a3d..f6db4a7 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,7 @@ { "name": "@tinyclaw/router", - "version": "1.0.0", + "private": true, + "version": "1.1.0", "description": "Multi-provider LLM routing and query classification for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/packages/sandbox/package.json b/packages/sandbox/package.json index 0dac5e6..6c569a9 100644 --- a/packages/sandbox/package.json +++ b/packages/sandbox/package.json @@ -1,6 +1,7 @@ { "name": "@tinyclaw/sandbox", - "version": "1.0.0", + "private": true, + "version": "1.1.0", "description": "Lightweight sandboxed code execution for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/packages/secrets/package.json b/packages/secrets/package.json index d527cb4..e8fc37e 100644 --- a/packages/secrets/package.json +++ b/packages/secrets/package.json @@ -1,6 +1,7 @@ { "name": "@tinyclaw/secrets", - "version": "1.0.0", + "private": true, + "version": "1.1.0", "description": "Machine-bound encrypted secrets management for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/packages/shell/package.json b/packages/shell/package.json index bb0e96a..0563848 100644 --- a/packages/shell/package.json +++ b/packages/shell/package.json @@ -1,6 +1,7 @@ { "name": "@tinyclaw/shell", - "version": "1.0.0", + "private": true, + "version": "1.1.0", "description": "Controlled shell execution with permission engine for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/packages/shield/package.json b/packages/shield/package.json index 100241f..f93891d 100644 --- a/packages/shield/package.json +++ b/packages/shield/package.json @@ -1,6 +1,7 @@ { "name": "@tinyclaw/shield", - "version": "1.0.0", + "private": true, + "version": "1.1.0", "description": "Runtime SHIELD.md threat evaluation and enforcement for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/packages/types/package.json b/packages/types/package.json index 63577c0..43db388 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@tinyclaw/types", - "version": "1.0.0", + "version": "1.1.0", "description": "Shared TypeScript type definitions for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 757af42..1c804fd 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -191,6 +191,8 @@ export interface AgentContext { getLatestSummary(userId: string): string | null; estimateTokens(text: string): number; }; + /** Pre-built system prompt section about available software updates. */ + updateContext?: string; } // --------------------------------------------------------------------------- diff --git a/plugins/channel/plugin-channel-discord/package.json b/plugins/channel/plugin-channel-discord/package.json index ce7d302..f9f426a 100644 --- a/plugins/channel/plugin-channel-discord/package.json +++ b/plugins/channel/plugin-channel-discord/package.json @@ -1,6 +1,6 @@ { "name": "@tinyclaw/plugin-channel-discord", - "version": "1.0.0", + "version": "1.1.0", "description": "Discord channel plugin for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/plugins/channel/plugin-channel-friends/package.json b/plugins/channel/plugin-channel-friends/package.json index 3c384e8..091d510 100644 --- a/plugins/channel/plugin-channel-friends/package.json +++ b/plugins/channel/plugin-channel-friends/package.json @@ -1,6 +1,6 @@ { "name": "@tinyclaw/plugin-channel-friends", - "version": "1.0.0", + "version": "1.1.0", "description": "Friends web chat channel plugin for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/plugins/provider/plugin-provider-openai/package.json b/plugins/provider/plugin-provider-openai/package.json index 7530f72..5b93e05 100644 --- a/plugins/provider/plugin-provider-openai/package.json +++ b/plugins/provider/plugin-provider-openai/package.json @@ -1,6 +1,6 @@ { "name": "@tinyclaw/plugin-provider-openai", - "version": "1.0.0", + "version": "1.1.0", "description": "OpenAI provider plugin for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/src/cli/package.json b/src/cli/package.json index 6350d57..52f1123 100644 --- a/src/cli/package.json +++ b/src/cli/package.json @@ -1,6 +1,6 @@ { "name": "tinyclaw", - "version": "1.0.0", + "version": "1.1.0", "description": "Command-line interface for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga", diff --git a/src/cli/src/commands/start.ts b/src/cli/src/commands/start.ts index 2512958..c2a635c 100644 --- a/src/cli/src/commands/start.ts +++ b/src/cli/src/commands/start.ts @@ -19,6 +19,8 @@ import { DEFAULT_MODEL, DEFAULT_BASE_URL, BUILTIN_MODEL_TAGS, + checkForUpdate, + buildUpdateContext, } from '@tinyclaw/core'; import { loadPlugins } from '@tinyclaw/plugins'; import { createPulseScheduler } from '@tinyclaw/pulse'; @@ -823,6 +825,19 @@ export async function startCommand(): Promise { // Load persisted owner ID (if already claimed) const persistedOwnerId = configManager.get('owner.ownerId'); + // --- Check for software updates (non-blocking) ------------------------- + + let updateContext: string | undefined; + try { + const { getVersion } = await import('../ui/banner.js'); + const currentVersion = getVersion(); + const updateInfo = await checkForUpdate(currentVersion, dataDir); + const ctx = buildUpdateContext(updateInfo); + if (ctx) updateContext = ctx; + } catch (err) { + logger.debug('Update check skipped', err); + } + const context = { db, provider: routerDefaultProvider, @@ -842,6 +857,7 @@ export async function startCommand(): Promise { shield, compactor, ownerId: persistedOwnerId || undefined, + updateContext, }; // --- Initialize pulse scheduler ----------------------------------------- diff --git a/src/landing/README.md b/src/landing/README.md new file mode 100644 index 0000000..7690a4e --- /dev/null +++ b/src/landing/README.md @@ -0,0 +1,9 @@ +# Tiny Claw Landing Page + +Official landing page for the Tiny Claw project, built with Svelte 5 + Vite + Tailwind CSS. + +## Scripts + +- `bun run dev` โ€” Start development server on port 5174 +- `bun run build` โ€” Build for production +- `bun run preview` โ€” Preview production build diff --git a/src/landing/index.html b/src/landing/index.html new file mode 100644 index 0000000..b4d1899 --- /dev/null +++ b/src/landing/index.html @@ -0,0 +1,16 @@ + + + + + + Tiny Claw โ€” Your Autonomous AI Companion + + + + + + +
+ + + diff --git a/src/landing/package.json b/src/landing/package.json new file mode 100644 index 0000000..32e44e1 --- /dev/null +++ b/src/landing/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tinyclaw/landing", + "private": true, + "version": "1.1.0", + "description": "Official landing page for Tiny Claw", + "license": "GPL-3.0", + "author": "Waren Gonzaga", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "svelte": "^5.20.1" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.1.4", + "@tailwindcss/vite": "^4.1.18", + "tailwindcss": "^4.1.18", + "vite": "^7.2.4" + } +} diff --git a/src/landing/public/CNAME b/src/landing/public/CNAME new file mode 100644 index 0000000..e87adda --- /dev/null +++ b/src/landing/public/CNAME @@ -0,0 +1 @@ +tinyclaw.ai diff --git a/src/landing/src/App.svelte b/src/landing/src/App.svelte new file mode 100644 index 0000000..ed287af --- /dev/null +++ b/src/landing/src/App.svelte @@ -0,0 +1,50 @@ + + + diff --git a/src/landing/src/app.css b/src/landing/src/app.css new file mode 100644 index 0000000..23147d4 --- /dev/null +++ b/src/landing/src/app.css @@ -0,0 +1,73 @@ +@import "tailwindcss"; + +@theme { + /* Ant Colony theme โ€” dark, earthy tones */ + --color-bg-primary: #0a0a0a; + --color-bg-secondary: #111111; + --color-bg-tertiary: #171717; + --color-bg-card: #141414; + --color-bg-card-hover: #1a1a1a; + --color-bg-code: #1e1e1e; + + --color-text-normal: #e0e0e0; + --color-text-muted: #8a8a8a; + --color-text-link: #c98540; + + --color-brand: #8b5a2b; + --color-brand-hover: #a06830; + --color-brand-light: #c98540; + --color-accent-green: #7ab648; + --color-accent-yellow: #c49530; + + --color-border: #222222; + --color-border-hover: #333333; + + --color-gradient-start: #8b5a2b; + --color-gradient-end: #c98540; +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +html, body { + margin: 0; + padding: 0; + background-color: var(--color-bg-primary); + color: var(--color-text-normal); + font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} + +#app { + min-height: 100vh; +} + +/* Custom scrollbar */ +* { + scrollbar-width: thin; + scrollbar-color: #262626 transparent; +} + +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: #262626; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--color-border-hover); +} diff --git a/src/landing/src/components/ComingSoon.svelte b/src/landing/src/components/ComingSoon.svelte new file mode 100644 index 0000000..0d3d2ea --- /dev/null +++ b/src/landing/src/components/ComingSoon.svelte @@ -0,0 +1,47 @@ +
+
+ +

+ Coming Soon +

+

+ Tiny Claw is under heavy development. We're building something special and want to get it right before the first official release. +

+ + +
+ +

Under Active Development

+

+ Running it now may spoil the experience we're building for you. Follow us for launch updates and be the first to know when Tiny Claw is ready. +

+ + + +
+ + +

+ Want to get involved? Check out the contributing guide or sponsor the project. +

+
+
diff --git a/src/landing/src/components/Features.svelte b/src/landing/src/components/Features.svelte new file mode 100644 index 0000000..0eee0b5 --- /dev/null +++ b/src/landing/src/components/Features.svelte @@ -0,0 +1,89 @@ + + +
+
+ +
+

+ Packed with Power +

+

+ A single ant is tiny, but it's autonomous, it learns, it adapts, and it builds something greater over time. +

+
+ + +
+ {#each features as feature} +
+
{feature.emoji}
+

{feature.title}

+

{feature.description}

+
+ {/each} +
+
+
diff --git a/src/landing/src/components/Footer.svelte b/src/landing/src/components/Footer.svelte new file mode 100644 index 0000000..bbd1cba --- /dev/null +++ b/src/landing/src/components/Footer.svelte @@ -0,0 +1,80 @@ + diff --git a/src/landing/src/components/Hero.svelte b/src/landing/src/components/Hero.svelte new file mode 100644 index 0000000..3c2eb4e --- /dev/null +++ b/src/landing/src/components/Hero.svelte @@ -0,0 +1,70 @@ +
+ + + +
+ +
+ + Under Active Development +
+ + + + + +

+ Tiny Claw +
+ Your Autonomous AI Companion +

+ + +

+ A personal, self-improving, self-learning AI companion built from scratch. + Small by design, mighty by nature. +

+ + + + + +
+
+
Native
+
Built from scratch
+
+
+
Tiny
+
Core stays minimal
+
+
+
Smart
+
Self-improving AI
+
+
+
Free
+
Open source
+
+
+
+
diff --git a/src/landing/src/components/QuickStart.svelte b/src/landing/src/components/QuickStart.svelte new file mode 100644 index 0000000..9d4ac55 --- /dev/null +++ b/src/landing/src/components/QuickStart.svelte @@ -0,0 +1,74 @@ +
+
+ +
+

+ Quick Start +

+

+ Up and running in under a minute. No config files needed โ€” Tiny Claw walks you through the rest. +

+
+ + +
+ +
+
+
1
+
+
+

Install

+
+ $ bun install +
+
+
+ + +
+
+
2
+
+
+

Run

+
+ $ bun start +
+
+
+ + +
+
+
3
+
+
+

Open

+

+ Visit http://localhost:3000 and Tiny Claw will guide you through the initial setup interactively. +

+
+
+
+ + +
+

Development

+
+
+
bun dev
+ Development mode with hot reload +
+
+
bun build
+ Build all packages +
+
+
bun test
+ Run the test suite +
+
+
+
+
diff --git a/src/landing/src/components/icons/GitHubIcon.svelte b/src/landing/src/components/icons/GitHubIcon.svelte new file mode 100644 index 0000000..dcddd4d --- /dev/null +++ b/src/landing/src/components/icons/GitHubIcon.svelte @@ -0,0 +1,6 @@ + + + diff --git a/src/landing/src/main.js b/src/landing/src/main.js new file mode 100644 index 0000000..8a8e537 --- /dev/null +++ b/src/landing/src/main.js @@ -0,0 +1,11 @@ +import './app.css' +import { mount } from 'svelte' +import App from './App.svelte' + +const target = document.getElementById('app') + +if (!target) { + throw new Error('Landing page failed to find #app root element.') +} + +mount(App, { target }) diff --git a/src/landing/tsconfig.json b/src/landing/tsconfig.json new file mode 100644 index 0000000..7d32988 --- /dev/null +++ b/src/landing/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "verbatimModuleSyntax": true + }, + "include": ["src/**/*.ts", "src/**/*.svelte"] +} diff --git a/src/landing/vite.config.ts b/src/landing/vite.config.ts new file mode 100644 index 0000000..2db8baf --- /dev/null +++ b/src/landing/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' +import tailwindcss from '@tailwindcss/vite' +import { resolve } from 'path' + +export default defineConfig({ + plugins: [svelte(), tailwindcss()], + build: { + rollupOptions: { + input: { + main: resolve(__dirname, 'index.html'), + }, + }, + }, + server: { + port: 5174, + }, +}) diff --git a/src/web/package.json b/src/web/package.json index a483cd6..79d8533 100644 --- a/src/web/package.json +++ b/src/web/package.json @@ -1,7 +1,7 @@ { "name": "@tinyclaw/web", "private": true, - "version": "1.0.0", + "version": "1.1.0", "description": "Web user interface for Tiny Claw", "license": "GPL-3.0", "author": "Waren Gonzaga",