feat: add prek pre-commit hooks and dependabot (#1720)

* feat: add prek pre-commit hooks and dependabot

Pre-commit hooks (via prek):
- Basic hygiene: trailing-whitespace, end-of-file-fixer, check-yaml, check-added-large-files, check-merge-conflict
- Security: detect-secrets, zizmor (GitHub Actions audit)
- Linting: shellcheck, actionlint, oxlint, swiftlint
- Formatting: oxfmt, swiftformat

Dependabot:
- npm and GitHub Actions ecosystems
- Grouped updates (production/development/actions)
- 7-day cooldown for supply chain protection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: add prek install instruction to AGENTS.md

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
main
Dan Guido 2026-01-25 05:53:23 -05:00 committed by GitHub
parent 612a27f3dd
commit 48aea87028
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
69 changed files with 2143 additions and 214 deletions

17
.github/actionlint.yaml vendored Normal file
View File

@ -0,0 +1,17 @@
# actionlint configuration
# https://github.com/rhysd/actionlint/blob/main/docs/config.md
self-hosted-runner:
labels:
# Blacksmith CI runners
- blacksmith-4vcpu-ubuntu-2404
- blacksmith-4vcpu-windows-2025
# Ignore patterns for known issues
paths:
.github/workflows/**/*.yml:
ignore:
# Ignore shellcheck warnings (we run shellcheck separately)
- 'shellcheck reported issue.+'
# Ignore intentional if: false for disabled jobs
- 'constant expression "false" in condition'

113
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,113 @@
# Dependabot configuration
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
registries:
npm-npmjs:
type: npm-registry
url: https://registry.npmjs.org
replaces-base: true
updates:
# npm dependencies (root)
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
production:
dependency-type: production
update-types:
- minor
- patch
development:
dependency-type: development
update-types:
- minor
- patch
open-pull-requests-limit: 10
registries:
- npm-npmjs
# GitHub Actions
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
actions:
patterns:
- "*"
update-types:
- minor
- patch
open-pull-requests-limit: 5
# Swift Package Manager - macOS app
- package-ecosystem: swift
directory: /apps/macos
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
swift-deps:
patterns:
- "*"
update-types:
- minor
- patch
open-pull-requests-limit: 5
# Swift Package Manager - shared ClawdbotKit
- package-ecosystem: swift
directory: /apps/shared/ClawdbotKit
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
swift-deps:
patterns:
- "*"
update-types:
- minor
- patch
open-pull-requests-limit: 5
# Swift Package Manager - Swabble
- package-ecosystem: swift
directory: /Swabble
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
swift-deps:
patterns:
- "*"
update-types:
- minor
- patch
open-pull-requests-limit: 5
# Gradle - Android app
- package-ecosystem: gradle
directory: /apps/android
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
android-deps:
patterns:
- "*"
update-types:
- minor
- patch
open-pull-requests-limit: 5

85
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,85 @@
# Pre-commit hooks for clawdbot
# Install: prek install
# Run manually: prek run --all-files
#
# See https://pre-commit.com for more information
repos:
# Basic file hygiene
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
exclude: '^(docs/|dist/|vendor/|.*\.snap$)'
- id: end-of-file-fixer
exclude: '^(docs/|dist/|vendor/|.*\.snap$)'
- id: check-yaml
args: [--allow-multiple-documents]
- id: check-added-large-files
args: [--maxkb=500]
- id: check-merge-conflict
# Secret detection (same as CI)
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: [--baseline, .secrets.baseline]
# Shell script linting
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.11.0
hooks:
- id: shellcheck
args: [--severity=error] # Only fail on errors, not warnings/info
# Exclude vendor and scripts with embedded code or known issues
exclude: '^(vendor/|scripts/e2e/)'
# GitHub Actions linting
- repo: https://github.com/rhysd/actionlint
rev: v1.7.10
hooks:
- id: actionlint
# GitHub Actions security audit
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.22.0
hooks:
- id: zizmor
args: [--persona=regular, --min-severity=medium, --min-confidence=medium]
exclude: '^(vendor/|Swabble/)'
# Project checks (same commands as CI)
- repo: local
hooks:
# oxlint --type-aware src test
- id: oxlint
name: oxlint
entry: npx oxlint --type-aware src test
language: system
pass_filenames: false
types_or: [javascript, jsx, ts, tsx]
# oxfmt --check src test
- id: oxfmt
name: oxfmt
entry: npx oxfmt --check src test
language: system
pass_filenames: false
types_or: [javascript, jsx, ts, tsx]
# swiftlint (same as CI)
- id: swiftlint
name: swiftlint
entry: swiftlint --config .swiftlint.yml
language: system
pass_filenames: false
types: [swift]
# swiftformat --lint (same as CI)
- id: swiftformat
name: swiftformat
entry: swiftformat --lint apps/macos/Sources --config .swiftformat
language: system
pass_filenames: false
types: [swift]

File diff suppressed because it is too large Load Diff

25
.shellcheckrc Normal file
View File

@ -0,0 +1,25 @@
# ShellCheck configuration
# https://www.shellcheck.net/wiki/
# Disable common false positives and style suggestions
# SC2034: Variable appears unused (often exported or used indirectly)
disable=SC2034
# SC2155: Declare and assign separately (common idiom, rarely causes issues)
disable=SC2155
# SC2295: Expansions inside ${..} need quoting (info-level, rarely causes issues)
disable=SC2295
# SC1012: \r is literal (tr -d '\r' works as intended on most systems)
disable=SC1012
# SC2026: Word outside quotes (info-level, often intentional)
disable=SC2026
# SC2016: Expressions don't expand in single quotes (often intentional in sed/awk)
disable=SC2016
# SC2129: Consider using { cmd1; cmd2; } >> file (style preference)
disable=SC2129

View File

@ -23,7 +23,7 @@
# Whitespace # Whitespace
--trimwhitespace always --trimwhitespace always
--emptybraces no-space --emptybraces no-space
--nospaceoperators ...,..< --nospaceoperators ...,..<
--ranges no-space --ranges no-space
--someAny true --someAny true
--voidtype void --voidtype void

View File

@ -37,6 +37,7 @@
## Build, Test, and Development Commands ## Build, Test, and Development Commands
- Runtime baseline: Node **22+** (keep Node + Bun paths working). - Runtime baseline: Node **22+** (keep Node + Bun paths working).
- Install deps: `pnpm install` - Install deps: `pnpm install`
- Pre-commit hooks: `prek install` (runs same checks as CI)
- Also supported: `bun install` (keep `pnpm-lock.yaml` + Bun patching in sync when touching deps/patches). - Also supported: `bun install` (keep `pnpm-lock.yaml` + Bun patching in sync when touching deps/patches).
- Prefer Bun for TypeScript execution (scripts, dev, tests): `bun <file.ts>` / `bunx <tool>`. - Prefer Bun for TypeScript execution (scripts, dev, tests): `bun <file.ts>` / `bunx <tool>`.
- Run CLI in dev: `pnpm clawdbot ...` (bun) or `pnpm dev`. - Run CLI in dev: `pnpm clawdbot ...` (bun) or `pnpm dev`.

View File

@ -459,7 +459,7 @@ Use these when youre past the onboarding flow and want the deeper reference.
## Clawd ## Clawd
Clawdbot was built for **Clawd**, a space lobster AI assistant. 🦞 Clawdbot was built for **Clawd**, a space lobster AI assistant. 🦞
by Peter Steinberger and the community. by Peter Steinberger and the community.
- [clawd.me](https://clawd.me) - [clawd.me](https://clawd.me)
@ -468,7 +468,7 @@ by Peter Steinberger and the community.
## Community ## Community
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines, maintainers, and how to submit PRs. See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines, maintainers, and how to submit PRs.
AI/vibe-coded PRs welcome! 🤖 AI/vibe-coded PRs welcome! 🤖
Special thanks to [Mario Zechner](https://mariozechner.at/) for his support and for Special thanks to [Mario Zechner](https://mariozechner.at/) for his support and for

View File

@ -12,4 +12,3 @@ If you believe youve found a security issue in Clawdbot, please report it pri
For threat model + hardening guidance (including `clawdbot security audit --deep` and `--fix`), see: For threat model + hardening guidance (including `clawdbot security audit --deep` and `--fix`), see:
- `https://docs.clawd.bot/gateway/security` - `https://docs.clawd.bot/gateway/security`

View File

@ -212,4 +212,4 @@
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.21/Clawdbot-2026.1.21.zip" length="22284796" type="application/octet-stream" sparkle:edSignature="pXji4NMA/cu35iMxln385d6LnsT4yIZtFtFiR7sIimKeSC2CsyeWzzSD0EhJsN98PdSoy69iEFZt4I2ZtNCECg=="/> <enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.21/Clawdbot-2026.1.21.zip" length="22284796" type="application/octet-stream" sparkle:edSignature="pXji4NMA/cu35iMxln385d6LnsT4yIZtFtFiR7sIimKeSC2CsyeWzzSD0EhJsN98PdSoy69iEFZt4I2ZtNCECg=="/>
</item> </item>
</channel> </channel>
</rss> </rss>

View File

@ -12,4 +12,3 @@ data class CameraHudState(
val kind: CameraHudKind, val kind: CameraHudKind,
val message: String, val message: String,
) )

View File

@ -12,4 +12,3 @@ enum class VoiceWakeMode(val rawValue: String) {
} }
} }
} }

View File

@ -135,7 +135,7 @@ class SmsManager(private val context: Context) {
/** /**
* Send an SMS message. * Send an SMS message.
* *
* @param paramsJson JSON with "to" (phone number) and "message" (text) fields * @param paramsJson JSON with "to" (phone number) and "message" (text) fields
* @return SendResult indicating success or failure * @return SendResult indicating success or failure
*/ */

View File

@ -1,4 +1,3 @@
<resources> <resources>
<color name="ic_launcher_background">#0A0A0A</color> <color name="ic_launcher_background">#0A0A0A</color>
</resources> </resources>

View File

@ -1,4 +1,3 @@
<resources> <resources>
<string name="app_name">Clawdbot Node</string> <string name="app_name">Clawdbot Node</string>
</resources> </resources>

View File

@ -23,4 +23,3 @@ class VoiceWakeCommandExtractorTest {
assertNull(VoiceWakeCommandExtractor.extractCommand("hey claude!", listOf("claude"))) assertNull(VoiceWakeCommandExtractor.extractCommand("hey claude!", listOf("claude")))
} }
} }

View File

@ -16,4 +16,3 @@ dependencyResolutionManagement {
rootProject.name = "ClawdbotNodeAndroid" rootProject.name = "ClawdbotNodeAndroid"
include(":app") include(":app")

View File

@ -3,4 +3,3 @@ parent_config: ../../.swiftlint.yml
included: included:
- Sources - Sources
- ../shared/ClawdisNodeKit/Sources - ../shared/ClawdisNodeKit/Sources

View File

@ -33,4 +33,4 @@
], ],
"squares" : "shared" "squares" : "shared"
} }
} }

View File

@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.

View File

@ -173,4 +173,4 @@
"iPod5,1": "iPod touch (5th generation)", "iPod5,1": "iPod touch (5th generation)",
"iPod7,1": "iPod touch (6th generation)", "iPod7,1": "iPod touch (6th generation)",
"iPod9,1": "iPod touch (7th generation)" "iPod9,1": "iPod touch (7th generation)"
} }

View File

@ -211,4 +211,4 @@
"Mac Pro (2019)", "Mac Pro (2019)",
"Mac Pro (Rack, 2019)" "Mac Pro (Rack, 2019)"
] ]
} }

View File

@ -33,7 +33,7 @@ export function registerMatrixAutoJoin(params: {
// For "allowlist" mode, handle invites manually // For "allowlist" mode, handle invites manually
client.on("room.invite", async (roomId: string, _inviteEvent: unknown) => { client.on("room.invite", async (roomId: string, _inviteEvent: unknown) => {
if (autoJoin !== "allowlist") return; if (autoJoin !== "allowlist") return;
// Get room alias if available // Get room alias if available
let alias: string | undefined; let alias: string | undefined;
let altAliases: string[] = []; let altAliases: string[] = [];

View File

@ -25,7 +25,7 @@ async function fetchMatrixMediaBuffer(params: {
// matrix-bot-sdk provides mxcToHttp helper // matrix-bot-sdk provides mxcToHttp helper
const url = params.client.mxcToHttp(params.mxcUrl); const url = params.client.mxcToHttp(params.mxcUrl);
if (!url) return null; if (!url) return null;
// Use the client's download method which handles auth // Use the client's download method which handles auth
try { try {
const buffer = await params.client.downloadContent(params.mxcUrl); const buffer = await params.client.downloadContent(params.mxcUrl);
@ -61,7 +61,7 @@ async function fetchEncryptedMediaBuffer(params: {
Buffer.from(encryptedBuffer), Buffer.from(encryptedBuffer),
params.file, params.file,
); );
return { buffer: decrypted }; return { buffer: decrypted };
} }
@ -77,7 +77,7 @@ export async function downloadMatrixMedia(params: {
placeholder: string; placeholder: string;
} | null> { } | null> {
let fetched: { buffer: Buffer; headerType?: string } | null; let fetched: { buffer: Buffer; headerType?: string } | null;
if (params.file) { if (params.file) {
// Encrypted media // Encrypted media
fetched = await fetchEncryptedMediaBuffer({ fetched = await fetchEncryptedMediaBuffer({
@ -93,7 +93,7 @@ export async function downloadMatrixMedia(params: {
maxBytes: params.maxBytes, maxBytes: params.maxBytes,
}); });
} }
if (!fetched) return null; if (!fetched) return null;
const headerType = fetched.headerType ?? params.contentType ?? undefined; const headerType = fetched.headerType ?? params.contentType ?? undefined;
const saved = await getMatrixRuntime().channel.media.saveMediaBuffer( const saved = await getMatrixRuntime().channel.media.saveMediaBuffer(

View File

@ -11,4 +11,4 @@ export function resolveMattermostGroupRequireMention(
}); });
if (typeof account.requireMention === "boolean") return account.requireMention; if (typeof account.requireMention === "boolean") return account.requireMention;
return true; return true;
} }

View File

@ -112,4 +112,4 @@ export function listEnabledMattermostAccounts(cfg: ClawdbotConfig): ResolvedMatt
return listMattermostAccountIds(cfg) return listMattermostAccountIds(cfg)
.map((accountId) => resolveMattermostAccount({ cfg, accountId })) .map((accountId) => resolveMattermostAccount({ cfg, accountId }))
.filter((account) => account.enabled); .filter((account) => account.enabled);
} }

View File

@ -205,4 +205,4 @@ export async function uploadMattermostFile(
throw new Error("Mattermost file upload failed"); throw new Error("Mattermost file upload failed");
} }
return info; return info;
} }

View File

@ -147,4 +147,4 @@ export function resolveThreadSessionKeys(params: {
? `${params.baseSessionKey}:thread:${threadId}` ? `${params.baseSessionKey}:thread:${threadId}`
: params.baseSessionKey; : params.baseSessionKey;
return { sessionKey, parentSessionKey: params.parentSessionKey }; return { sessionKey, parentSessionKey: params.parentSessionKey };
} }

View File

@ -67,4 +67,4 @@ export async function probeMattermost(
} finally { } finally {
if (timer) clearTimeout(timer); if (timer) clearTimeout(timer);
} }
} }

View File

@ -39,4 +39,4 @@ export async function promptAccountId(params: PromptAccountIdParams): Promise<st
); );
} }
return normalized; return normalized;
} }

View File

@ -184,4 +184,4 @@ export const mattermostOnboardingAdapter: ChannelOnboardingAdapter = {
mattermost: { ...cfg.channels?.mattermost, enabled: false }, mattermost: { ...cfg.channels?.mattermost, enabled: false },
}, },
}), }),
}; };

View File

@ -22,11 +22,11 @@ parallel:
security = session: security_expert security = session: security_expert
prompt: "Perform a deep security audit of the changes. Look for OWASP top 10 issues." prompt: "Perform a deep security audit of the changes. Look for OWASP top 10 issues."
context: overview context: overview
perf = session: performance_expert perf = session: performance_expert
prompt: "Analyze the performance implications. Identify potential bottlenecks or regressions." prompt: "Analyze the performance implications. Identify potential bottlenecks or regressions."
context: overview context: overview
style = session: reviewer style = session: reviewer
prompt: "Review for code style, maintainability, and adherence to best practices." prompt: "Review for code style, maintainability, and adherence to best practices."
context: overview context: overview

View File

@ -19,4 +19,3 @@ export type CallManagerContext = {
transcriptWaiters: Map<CallId, TranscriptWaiter>; transcriptWaiters: Map<CallId, TranscriptWaiter>;
maxDurationTimers: Map<CallId, NodeJS.Timeout>; maxDurationTimers: Map<CallId, NodeJS.Timeout>;
}; };

View File

@ -175,4 +175,3 @@ export function processEvent(ctx: CallManagerContext, event: NormalizedEvent): v
persistCallRecord(ctx.storePath, call); persistCallRecord(ctx.storePath, call);
} }

View File

@ -31,4 +31,3 @@ export function findCall(params: {
providerCallId: params.callIdOrProviderCallId, providerCallId: params.callIdOrProviderCallId,
}); });
} }

View File

@ -48,4 +48,3 @@ export function addTranscriptEntry(
}; };
call.transcript.push(entry); call.transcript.push(entry);
} }

View File

@ -86,4 +86,3 @@ export async function getCallHistoryFromStore(
return calls; return calls;
} }

View File

@ -84,4 +84,3 @@ export function waitForFinalTranscript(
ctx.transcriptWaiters.set(callId, { resolve, reject, timeout }); ctx.transcriptWaiters.set(callId, { resolve, reject, timeout });
}); });
} }

View File

@ -7,4 +7,3 @@ export function generateNotifyTwiml(message: string, voice: string): string {
<Hangup/> <Hangup/>
</Response>`; </Response>`;
} }

View File

@ -26,4 +26,3 @@ describe("PlivoProvider", () => {
expect(result.providerResponseBody).toContain('length="300"'); expect(result.providerResponseBody).toContain('length="300"');
}); });
}); });

View File

@ -27,4 +27,3 @@ export function verifyTwilioProviderWebhook(params: {
reason: result.reason, reason: result.reason,
}; };
} }

View File

@ -15,4 +15,3 @@ describe("zalouser outbound chunker", () => {
expect(chunks.every((c) => c.length <= limit)).toBe(true); expect(chunks.every((c) => c.length <= limit)).toBe(true);
}); });
}); });

View File

@ -124,7 +124,7 @@ EOF
# Function to list categories # Function to list categories
list_categories() { list_categories() {
echo -e "${BLUE}Fetching VibeTunnel log categories from the last hour...${NC}\n" echo -e "${BLUE}Fetching VibeTunnel log categories from the last hour...${NC}\n"
# Get unique categories from recent logs # Get unique categories from recent logs
log show --predicate "subsystem == \"$SUBSYSTEM\"" --last 1h 2>/dev/null | \ log show --predicate "subsystem == \"$SUBSYSTEM\"" --last 1h 2>/dev/null | \
grep -E "category: \"[^\"]+\"" | \ grep -E "category: \"[^\"]+\"" | \
@ -133,7 +133,7 @@ list_categories() {
while read -r cat; do while read -r cat; do
echo "$cat" echo "$cat"
done done
echo -e "\n${YELLOW}Note: Only categories with recent activity are shown${NC}" echo -e "\n${YELLOW}Note: Only categories with recent activity are shown${NC}"
} }
@ -230,29 +230,29 @@ fi
if [[ "$STREAM_MODE" == true ]]; then if [[ "$STREAM_MODE" == true ]]; then
# Streaming mode # Streaming mode
CMD="sudo log stream --predicate '$PREDICATE' --level $LOG_LEVEL --info" CMD="sudo log stream --predicate '$PREDICATE' --level $LOG_LEVEL --info"
echo -e "${GREEN}Streaming VibeTunnel logs continuously...${NC}" echo -e "${GREEN}Streaming VibeTunnel logs continuously...${NC}"
echo -e "${YELLOW}Press Ctrl+C to stop${NC}\n" echo -e "${YELLOW}Press Ctrl+C to stop${NC}\n"
else else
# Show mode # Show mode
CMD="sudo log show --predicate '$PREDICATE'" CMD="sudo log show --predicate '$PREDICATE'"
# Add log level for show command # Add log level for show command
if [[ "$LOG_LEVEL" == "debug" ]]; then if [[ "$LOG_LEVEL" == "debug" ]]; then
CMD="$CMD --debug" CMD="$CMD --debug"
else else
CMD="$CMD --info" CMD="$CMD --info"
fi fi
# Add time range # Add time range
CMD="$CMD --last $TIME_RANGE" CMD="$CMD --last $TIME_RANGE"
if [[ "$SHOW_TAIL" == true ]]; then if [[ "$SHOW_TAIL" == true ]]; then
echo -e "${GREEN}Showing last $TAIL_LINES log lines from the past $TIME_RANGE${NC}" echo -e "${GREEN}Showing last $TAIL_LINES log lines from the past $TIME_RANGE${NC}"
else else
echo -e "${GREEN}Showing all logs from the past $TIME_RANGE${NC}" echo -e "${GREEN}Showing all logs from the past $TIME_RANGE${NC}"
fi fi
# Show applied filters # Show applied filters
if [[ "$ERRORS_ONLY" == true ]]; then if [[ "$ERRORS_ONLY" == true ]]; then
echo -e "${RED}Filter: Errors only${NC}" echo -e "${RED}Filter: Errors only${NC}"
@ -277,14 +277,14 @@ if [[ -n "$OUTPUT_FILE" ]]; then
if sudo -n /usr/bin/log show --last 1s 2>&1 | grep -q "password"; then if sudo -n /usr/bin/log show --last 1s 2>&1 | grep -q "password"; then
handle_sudo_error handle_sudo_error
fi fi
echo -e "${BLUE}Exporting logs to: $OUTPUT_FILE${NC}\n" echo -e "${BLUE}Exporting logs to: $OUTPUT_FILE${NC}\n"
if [[ "$SHOW_TAIL" == true ]] && [[ "$STREAM_MODE" == false ]]; then if [[ "$SHOW_TAIL" == true ]] && [[ "$STREAM_MODE" == false ]]; then
eval "$CMD" 2>&1 | tail -n "$TAIL_LINES" > "$OUTPUT_FILE" eval "$CMD" 2>&1 | tail -n "$TAIL_LINES" > "$OUTPUT_FILE"
else else
eval "$CMD" > "$OUTPUT_FILE" 2>&1 eval "$CMD" > "$OUTPUT_FILE" 2>&1
fi fi
# Check if file was created and has content # Check if file was created and has content
if [[ -s "$OUTPUT_FILE" ]]; then if [[ -s "$OUTPUT_FILE" ]]; then
LINE_COUNT=$(wc -l < "$OUTPUT_FILE" | tr -d ' ') LINE_COUNT=$(wc -l < "$OUTPUT_FILE" | tr -d ' ')
@ -298,7 +298,7 @@ else
if sudo -n /usr/bin/log show --last 1s 2>&1 | grep -q "password"; then if sudo -n /usr/bin/log show --last 1s 2>&1 | grep -q "password"; then
handle_sudo_error handle_sudo_error
fi fi
if [[ "$SHOW_TAIL" == true ]] && [[ "$STREAM_MODE" == false ]]; then if [[ "$SHOW_TAIL" == true ]] && [[ "$STREAM_MODE" == false ]]; then
# Apply tail for non-streaming mode # Apply tail for non-streaming mode
eval "$CMD" 2>&1 | tail -n "$TAIL_LINES" eval "$CMD" 2>&1 | tail -n "$TAIL_LINES"

View File

@ -102,12 +102,12 @@ ws.send(
); );
const connectRes = await onceFrame((o) => o?.type === \"res\" && o?.id === \"c1\"); const connectRes = await onceFrame((o) => o?.type === \"res\" && o?.id === \"c1\");
if (!connectRes.ok) throw new Error(\"connect failed: \" + (connectRes.error?.message ?? \"unknown\")); if (!connectRes.ok) throw new Error(\"connect failed: \" + (connectRes.error?.message ?? \"unknown\"));
ws.send(JSON.stringify({ type: \"req\", id: \"h1\", method: \"health\" })); ws.send(JSON.stringify({ type: \"req\", id: \"h1\", method: \"health\" }));
const healthRes = await onceFrame((o) => o?.type === \"res\" && o?.id === \"h1\", 10000); const healthRes = await onceFrame((o) => o?.type === \"res\" && o?.id === \"h1\", 10000);
if (!healthRes.ok) throw new Error(\"health failed: \" + (healthRes.error?.message ?? \"unknown\")); if (!healthRes.ok) throw new Error(\"health failed: \" + (healthRes.error?.message ?? \"unknown\"));
if (healthRes.payload?.ok !== true) throw new Error(\"unexpected health payload\"); if (healthRes.payload?.ok !== true) throw new Error(\"unexpected health payload\");
ws.close(); ws.close();
console.log(\"ok\"); console.log(\"ok\");
NODE" NODE"

View File

@ -30,4 +30,3 @@ export type Entry = {
avatar_url: string; avatar_url: string;
lines: number; lines: number;
}; };

View File

@ -84,7 +84,7 @@ curl http://127.0.0.1:8000/places/{place_id}
"open_now": true "open_now": true
} }
], ],
"next_page_token": "..." "next_page_token": "..."
} }
``` ```

View File

@ -1,3 +1,2 @@
import "./styles.css"; import "./styles.css";
import "./ui/app.ts"; import "./ui/app.ts";

View File

@ -279,4 +279,3 @@
min-width: 120px; min-width: 120px;
} }
} }

View File

@ -122,4 +122,3 @@
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
margin: 1em 0; margin: 1em 0;
} }

View File

@ -196,4 +196,3 @@
transform: scale(1); transform: scale(1);
} }
} }

View File

@ -3,4 +3,3 @@ export type EventLogEntry = {
event: string; event: string;
payload?: unknown; payload?: unknown;
}; };

View File

@ -154,13 +154,13 @@ const COMPACTION_TOAST_DURATION_MS = 5000;
export function handleCompactionEvent(host: CompactionHost, payload: AgentEventPayload) { export function handleCompactionEvent(host: CompactionHost, payload: AgentEventPayload) {
const data = payload.data ?? {}; const data = payload.data ?? {};
const phase = typeof data.phase === "string" ? data.phase : ""; const phase = typeof data.phase === "string" ? data.phase : "";
// Clear any existing timer // Clear any existing timer
if (host.compactionClearTimer != null) { if (host.compactionClearTimer != null) {
window.clearTimeout(host.compactionClearTimer); window.clearTimeout(host.compactionClearTimer);
host.compactionClearTimer = null; host.compactionClearTimer = null;
} }
if (phase === "start") { if (phase === "start") {
host.compactionStatus = { host.compactionStatus = {
active: true, active: true,
@ -183,13 +183,13 @@ export function handleCompactionEvent(host: CompactionHost, payload: AgentEventP
export function handleAgentEvent(host: ToolStreamHost, payload?: AgentEventPayload) { export function handleAgentEvent(host: ToolStreamHost, payload?: AgentEventPayload) {
if (!payload) return; if (!payload) return;
// Handle compaction events // Handle compaction events
if (payload.stream === "compaction") { if (payload.stream === "compaction") {
handleCompactionEvent(host as CompactionHost, payload); handleCompactionEvent(host as CompactionHost, payload);
return; return;
} }
if (payload.stream !== "tool") return; if (payload.stream !== "tool") return;
const sessionKey = const sessionKey =
typeof payload.sessionKey === "string" ? payload.sessionKey : undefined; typeof payload.sessionKey === "string" ? payload.sessionKey : undefined;

View File

@ -74,4 +74,3 @@ export function removePathValue(
delete (current as Record<string, unknown>)[lastKey]; delete (current as Record<string, unknown>)[lastKey];
} }
} }

View File

@ -54,4 +54,3 @@ export async function callDebugMethod(state: DebugState) {
state.debugCallError = String(err); state.debugCallError = String(err);
} }
} }

View File

@ -33,4 +33,3 @@ export async function loadPresence(state: PresenceState) {
state.presenceLoading = false; state.presenceLoading = false;
} }
} }

View File

@ -39,4 +39,3 @@ describe("stripThinkingTags", () => {
expect(stripThinkingTags("Hello</final>")).toBe("Hello"); expect(stripThinkingTags("Hello</final>")).toBe("Hello");
}); });
}); });

View File

@ -30,4 +30,3 @@ describe("toSanitizedMarkdownHtml", () => {
expect(html).toContain("console.log(1)"); expect(html).toContain("console.log(1)");
}); });
}); });

View File

@ -55,4 +55,3 @@ export function formatCronPayload(job: CronJob) {
if (p.kind === "systemEvent") return `System: ${p.text}`; if (p.kind === "systemEvent") return `System: ${p.text}`;
return `Agent: ${p.message}`; return `Agent: ${p.message}`;
} }

View File

@ -30,4 +30,3 @@ describe("generateUUID", () => {
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
}); });
}); });

View File

@ -40,4 +40,3 @@ export function generateUUID(cryptoLike: CryptoLike | null = globalThis.crypto):
return uuidFromBytes(weakRandomBytes()); return uuidFromBytes(weakRandomBytes());
} }

View File

@ -43,4 +43,3 @@ export function renderChannelAccountCount(
if (count < 2) return nothing; if (count < 2) return nothing;
return html`<div class="account-count">Accounts (${count})</div>`; return html`<div class="account-count">Accounts (${count})</div>`;
} }

View File

@ -116,4 +116,3 @@ export function renderWhatsAppCard(params: {
</div> </div>
`; `;
} }

View File

@ -70,7 +70,7 @@ const COMPACTION_TOAST_DURATION_MS = 5000;
function renderCompactionIndicator(status: CompactionIndicatorStatus | null | undefined) { function renderCompactionIndicator(status: CompactionIndicatorStatus | null | undefined) {
if (!status) return nothing; if (!status) return nothing;
// Show "compacting..." while active // Show "compacting..." while active
if (status.active) { if (status.active) {
return html` return html`
@ -91,7 +91,7 @@ function renderCompactionIndicator(status: CompactionIndicatorStatus | null | un
`; `;
} }
} }
return nothing; return nothing;
} }

View File

@ -120,7 +120,7 @@ export function renderNode(params: {
const hasString = normalizedTypes.has("string"); const hasString = normalizedTypes.has("string");
const hasNumber = normalizedTypes.has("number"); const hasNumber = normalizedTypes.has("number");
const hasBoolean = normalizedTypes.has("boolean"); const hasBoolean = normalizedTypes.has("boolean");
if (hasBoolean && normalizedTypes.size === 1) { if (hasBoolean && normalizedTypes.size === 1) {
return renderNode({ return renderNode({
...params, ...params,
@ -383,14 +383,14 @@ function renderObject(params: {
const hint = hintForPath(path, hints); const hint = hintForPath(path, hints);
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1))); const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
const help = hint?.help ?? schema.description; const help = hint?.help ?? schema.description;
const fallback = value ?? schema.default; const fallback = value ?? schema.default;
const obj = fallback && typeof fallback === "object" && !Array.isArray(fallback) const obj = fallback && typeof fallback === "object" && !Array.isArray(fallback)
? (fallback as Record<string, unknown>) ? (fallback as Record<string, unknown>)
: {}; : {};
const props = schema.properties ?? {}; const props = schema.properties ?? {};
const entries = Object.entries(props); const entries = Object.entries(props);
// Sort by hint order // Sort by hint order
const sorted = entries.sort((a, b) => { const sorted = entries.sort((a, b) => {
const orderA = hintForPath([...path, a[0]], hints)?.order ?? 0; const orderA = hintForPath([...path, a[0]], hints)?.order ?? 0;
@ -514,7 +514,7 @@ function renderArray(params: {
</button> </button>
</div> </div>
${help ? html`<div class="cfg-array__help">${help}</div>` : nothing} ${help ? html`<div class="cfg-array__help">${help}</div>` : nothing}
${arr.length === 0 ? html` ${arr.length === 0 ? html`
<div class="cfg-array__empty"> <div class="cfg-array__empty">
No items yet. Click "Add" to create one. No items yet. Click "Add" to create one.
@ -597,7 +597,7 @@ function renderMapField(params: {
Add Entry Add Entry
</button> </button>
</div> </div>
${entries.length === 0 ? html` ${entries.length === 0 ? html`
<div class="cfg-map__empty">No custom entries.</div> <div class="cfg-map__empty">No custom entries.</div>
` : html` ` : html`

View File

@ -94,16 +94,16 @@ function matchesSearch(key: string, schema: JsonSchema, query: string): boolean
if (!query) return true; if (!query) return true;
const q = query.toLowerCase(); const q = query.toLowerCase();
const meta = SECTION_META[key]; const meta = SECTION_META[key];
// Check key name // Check key name
if (key.toLowerCase().includes(q)) return true; if (key.toLowerCase().includes(q)) return true;
// Check label and description // Check label and description
if (meta) { if (meta) {
if (meta.label.toLowerCase().includes(q)) return true; if (meta.label.toLowerCase().includes(q)) return true;
if (meta.description.toLowerCase().includes(q)) return true; if (meta.description.toLowerCase().includes(q)) return true;
} }
return schemaMatches(schema, q); return schemaMatches(schema, q);
} }
@ -192,8 +192,8 @@ export function renderConfigForm(props: ConfigFormProps) {
<div class="config-empty"> <div class="config-empty">
<div class="config-empty__icon">${icons.search}</div> <div class="config-empty__icon">${icons.search}</div>
<div class="config-empty__text"> <div class="config-empty__text">
${searchQuery ${searchQuery
? `No settings match "${searchQuery}"` ? `No settings match "${searchQuery}"`
: "No settings in this section"} : "No settings in this section"}
</div> </div>
</div> </div>

View File

@ -89,4 +89,3 @@ export function isSensitivePath(path: Array<string | number>): boolean {
key.endsWith("key") key.endsWith("key")
); );
} }

View File

@ -5,4 +5,3 @@ export {
} from "./config-form.analyze"; } from "./config-form.analyze";
export { renderNode } from "./config-form.node"; export { renderNode } from "./config-form.node";
export { schemaType, type JsonSchema } from "./config-form.shared"; export { schemaType, type JsonSchema } from "./config-form.shared";

View File

@ -138,7 +138,7 @@ function computeDiff(
): Array<{ path: string; from: unknown; to: unknown }> { ): Array<{ path: string; from: unknown; to: unknown }> {
if (!original || !current) return []; if (!original || !current) return [];
const changes: Array<{ path: string; from: unknown; to: unknown }> = []; const changes: Array<{ path: string; from: unknown; to: unknown }> = [];
function compare(orig: unknown, curr: unknown, path: string) { function compare(orig: unknown, curr: unknown, path: string) {
if (orig === curr) return; if (orig === curr) return;
if (typeof orig !== typeof curr) { if (typeof orig !== typeof curr) {
@ -164,7 +164,7 @@ function computeDiff(
compare(origObj[key], currObj[key], path ? `${path}.${key}` : key); compare(origObj[key], currObj[key], path ? `${path}.${key}` : key);
} }
} }
compare(original, current, ""); compare(original, current, "");
return changes; return changes;
} }
@ -258,7 +258,7 @@ export function renderConfig(props: ConfigProps) {
<div class="config-sidebar__title">Settings</div> <div class="config-sidebar__title">Settings</div>
<span class="pill pill--sm ${validity === "valid" ? "pill--ok" : validity === "invalid" ? "pill--danger" : ""}">${validity}</span> <span class="pill pill--sm ${validity === "valid" ? "pill--ok" : validity === "invalid" ? "pill--danger" : ""}">${validity}</span>
</div> </div>
<!-- Search --> <!-- Search -->
<div class="config-search"> <div class="config-search">
<svg class="config-search__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="config-search__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -273,13 +273,13 @@ export function renderConfig(props: ConfigProps) {
@input=${(e: Event) => props.onSearchChange((e.target as HTMLInputElement).value)} @input=${(e: Event) => props.onSearchChange((e.target as HTMLInputElement).value)}
/> />
${props.searchQuery ? html` ${props.searchQuery ? html`
<button <button
class="config-search__clear" class="config-search__clear"
@click=${() => props.onSearchChange("")} @click=${() => props.onSearchChange("")}
>×</button> >×</button>
` : nothing} ` : nothing}
</div> </div>
<!-- Section nav --> <!-- Section nav -->
<nav class="config-nav"> <nav class="config-nav">
<button <button
@ -299,7 +299,7 @@ export function renderConfig(props: ConfigProps) {
</button> </button>
`)} `)}
</nav> </nav>
<!-- Mode toggle at bottom --> <!-- Mode toggle at bottom -->
<div class="config-sidebar__footer"> <div class="config-sidebar__footer">
<div class="config-mode-toggle"> <div class="config-mode-toggle">
@ -319,7 +319,7 @@ export function renderConfig(props: ConfigProps) {
</div> </div>
</div> </div>
</aside> </aside>
<!-- Main content --> <!-- Main content -->
<main class="config-main"> <main class="config-main">
<!-- Action bar --> <!-- Action bar -->
@ -358,7 +358,7 @@ export function renderConfig(props: ConfigProps) {
</button> </button>
</div> </div>
</div> </div>
<!-- Diff panel (form mode only - raw mode doesn't have granular diff) --> <!-- Diff panel (form mode only - raw mode doesn't have granular diff) -->
${hasChanges && props.formMode === "form" ? html` ${hasChanges && props.formMode === "form" ? html`
<details class="config-diff"> <details class="config-diff">

17
zizmor.yml Normal file
View File

@ -0,0 +1,17 @@
# zizmor configuration
# https://docs.zizmor.sh/configuration/
rules:
# Disable unpinned-uses - pinning to SHA hashes is a significant change
# that should be done deliberately, not enforced by pre-commit
unpinned-uses:
disable: true
# Disable excessive-permissions for now - adding explicit permissions
# blocks requires careful review of each workflow's needs
excessive-permissions:
disable: true
# Disable artipacked (persist-credentials) - low confidence finding
artipacked:
disable: true