Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0d15382
📦 new: add update checker functionality and context injection
warengonzaga Feb 18, 2026
ec863b1
📦 new: implement update checker with caching and runtime detection
warengonzaga Feb 18, 2026
054b7c8
🧪 test: add comprehensive tests for update checker module
warengonzaga Feb 18, 2026
59c79d9
📦 new: add updateContext to AgentContext for software updates
warengonzaga Feb 18, 2026
c0a9883
📦 new: add software update check and context to start command
warengonzaga Feb 18, 2026
9398d21
📦 new: update package versions to 1.0.1 across all packages
warengonzaga Feb 18, 2026
4e31c8a
⚙️ setup: update CI workflows for release process and permissions
warengonzaga Feb 18, 2026
1cd2fca
🔧 update: enhance version parsing in isNewerVersion function
warengonzaga Feb 18, 2026
3f91a71
📦 new: add build step for all packages in CI workflow
warengonzaga Feb 18, 2026
080224e
⚙️ setup (ci): update package-build-flow-action to v2.0.1
warengonzaga Feb 19, 2026
6e46e61
⚙️ setup: enforce clean commit convention with husky and ci
warengonzaga Feb 19, 2026
57d3d0f
⚙️ setup (husky): add clean commit validation hook
warengonzaga Feb 19, 2026
d71d865
⚙️ setup (ci): fix security, guards, and validation issues
warengonzaga Feb 19, 2026
0407858
⚙️ setup (ci): remove redundant permissions from reusable workflow calls
warengonzaga Feb 19, 2026
a240bb6
🔧 update (ci): handle initial push and improve update-checker test
warengonzaga Feb 19, 2026
283a069
📦 new (landing): add landing page with svelte and tailwindcss
warengonzaga Feb 19, 2026
cbe57e2
⚙️ setup (ci): add deploy workflow for landing page
warengonzaga Feb 19, 2026
01135d6
⚙️ setup (landing): add build:landing script and update lockfile
warengonzaga Feb 19, 2026
af268be
⚙️ setup (husky): add error handling and allow revert commits
warengonzaga Feb 19, 2026
cf70b85
🔒 security (update-checker): sanitize version and url for prompt inje…
warengonzaga Feb 19, 2026
7094636
🧪 test (update-checker): add sanitizeForPrompt tests and improve mocking
warengonzaga Feb 19, 2026
858ff22
⚙️ setup (ci): pin action shas and scope deploy permissions
warengonzaga Feb 19, 2026
a5c1305
🔒 security (core): harden update checker cache and fetch logic
warengonzaga Feb 19, 2026
ea45a35
📦 new (landing): extract GitHubIcon component and improve accessibility
warengonzaga Feb 19, 2026
c142513
🔧 update (landing): improve scrollbar styling and Firefox support
warengonzaga Feb 19, 2026
302f44b
🔧 update (landing): clarify QuickStart step 3 description
warengonzaga Feb 19, 2026
99c228a
⚙️ setup (ci): streamline CI workflows by removing unused jobs
warengonzaga Feb 19, 2026
bab10e0
🗑️ remove: delete pre-commit script for bun test
warengonzaga Feb 19, 2026
3004a38
🔧 update (ci): add build step for workspace packages
warengonzaga Feb 19, 2026
67f0a76
🔧 update (dockerfile): upgrade bun version for builder and production…
warengonzaga Feb 19, 2026
8dc6789
🚀 release: bump version to 1.1.0 for all packages
warengonzaga Feb 19, 2026
4422bf3
🔧 update (dockerfile): remove frozen-lockfile option from bun install
warengonzaga Feb 19, 2026
958e520
📦 new: add dev:landing script for development of landing page
warengonzaga Feb 19, 2026
61699d8
📖 docs: update README with model names and licensing information
warengonzaga Feb 19, 2026
6e3e0bd
⚙️ setup: update release action to v1.2.1 and change token secret
warengonzaga Feb 20, 2026
8bb8382
📖 docs: update commit message guidelines for breaking changes
warengonzaga Feb 20, 2026
7c8f470
🔧 update: enhance commit message validation for breaking changes
warengonzaga Feb 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tinyclaw-monorepo",
"version": "1.0.0",
"version": "1.0.1",
"description": "Your autonomous AI companion",
"license": "GPL-3.0",
"author": "Waren Gonzaga",
Expand Down
3 changes: 2 additions & 1 deletion packages/compactor/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "@tinyclaw/compactor",
"version": "1.0.0",
"private": true,
"version": "1.0.1",
"description": "Layered conversation compaction and token optimization for Tiny Claw",
"license": "GPL-3.0",
"author": "Waren Gonzaga",
Expand Down
3 changes: 2 additions & 1 deletion packages/config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "@tinyclaw/config",
"version": "1.0.0",
"private": true,
"version": "1.0.1",
"description": "SQLite-backed persistent configuration for Tiny Claw",
"license": "GPL-3.0",
"author": "Waren Gonzaga",
Expand Down
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "@tinyclaw/core",
"version": "1.0.0",
"private": true,
"version": "1.0.1",
"description": "Core agent runtime for Tiny Claw",
"license": "GPL-3.0",
"author": "Waren Gonzaga",
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
5 changes: 5 additions & 0 deletions packages/core/src/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
255 changes: 255 additions & 0 deletions packages/core/src/update-checker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
/**
* 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)
// ---------------------------------------------------------------------------

/**
* 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/, '').split('.').map(Number).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;
if (cached && typeof cached.checkedAt === 'number') return cached;
} catch {
// Missing or corrupt — will re-check
}
return null;
}

function writeCache(dataDir: string, info: UpdateInfo): void {
try {
const dir = join(dataDir, 'data');
if (!existsSync(dir)) 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<string | null> {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);

const res = await fetch(NPM_REGISTRY_URL, {
signal: controller.signal,
headers: { Accept: 'application/json' },
});
clearTimeout(timeout);

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

// ---------------------------------------------------------------------------
// 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<UpdateInfo | null> {
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) return cached ?? null; // Network failure — use stale cache if available

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 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:** ${info.current}
- **Latest version:** ${info.latest}
- **Runtime:** ${info.runtime}
- **Release notes:** ${info.releaseUrl}

${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.`;
}
Loading