Compare commits
7 Commits
feat/prek-
...
fix/node-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca26e17273 | ||
|
|
34ab1d245c | ||
|
|
a11b98f801 | ||
|
|
67db63ba05 | ||
|
|
bbefb2e5a5 | ||
|
|
50f233d16d | ||
|
|
48aea87028 |
@@ -7,6 +7,10 @@
|
||||
[exclude-files]
|
||||
# pnpm lockfiles contain lots of high-entropy package integrity blobs.
|
||||
pattern = (^|/)pnpm-lock\.yaml$
|
||||
# Generated output and vendored assets.
|
||||
pattern = (^|/)(dist|vendor)/
|
||||
# Local config file with allowlist patterns.
|
||||
pattern = (^|/)\.detect-secrets\.cfg$
|
||||
|
||||
[exclude-lines]
|
||||
# Fastlane checks for private key marker; not a real key.
|
||||
|
||||
17
.github/actionlint.yaml
vendored
Normal file
17
.github/actionlint.yaml
vendored
Normal 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
113
.github/dependabot.yml
vendored
Normal 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
|
||||
105
.pre-commit-config.yaml
Normal file
105
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,105 @@
|
||||
# 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
|
||||
- --exclude-files
|
||||
- '(^|/)(dist/|vendor/|pnpm-lock\.yaml$|\.detect-secrets\.cfg$)'
|
||||
- --exclude-lines
|
||||
- 'key_content\.include\?\("BEGIN PRIVATE KEY"\)'
|
||||
- --exclude-lines
|
||||
- 'case \.apiKeyEnv: "API key \(env var\)"'
|
||||
- --exclude-lines
|
||||
- 'case apikey = "apiKey"'
|
||||
- --exclude-lines
|
||||
- '"gateway\.remote\.password"'
|
||||
- --exclude-lines
|
||||
- '"gateway\.auth\.password"'
|
||||
- --exclude-lines
|
||||
- '"talk\.apiKey"'
|
||||
- --exclude-lines
|
||||
- '=== "string"'
|
||||
- --exclude-lines
|
||||
- 'typeof remote\?\.password === "string"'
|
||||
|
||||
# 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: scripts/pre-commit/run-node-tool.sh 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: scripts/pre-commit/run-node-tool.sh 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]
|
||||
1923
.secrets.baseline
1923
.secrets.baseline
File diff suppressed because it is too large
Load Diff
25
.shellcheckrc
Normal file
25
.shellcheckrc
Normal 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
|
||||
@@ -23,7 +23,7 @@
|
||||
# Whitespace
|
||||
--trimwhitespace always
|
||||
--emptybraces no-space
|
||||
--nospaceoperators ...,..<
|
||||
--nospaceoperators ...,..<
|
||||
--ranges no-space
|
||||
--someAny true
|
||||
--voidtype void
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
## Build, Test, and Development Commands
|
||||
- Runtime baseline: Node **22+** (keep Node + Bun paths working).
|
||||
- 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).
|
||||
- Prefer Bun for TypeScript execution (scripts, dev, tests): `bun <file.ts>` / `bunx <tool>`.
|
||||
- Run CLI in dev: `pnpm clawdbot ...` (bun) or `pnpm dev`.
|
||||
|
||||
@@ -12,6 +12,7 @@ Docs: https://docs.clawd.bot
|
||||
- TTS: add Edge TTS provider fallback, defaulting to keyless Edge with MP3 retry on format failures. (#1668) Thanks @steipete. https://docs.clawd.bot/tts
|
||||
- Web search: add Brave freshness filter parameter for time-scoped results. (#1688) Thanks @JonUleis. https://docs.clawd.bot/tools/web
|
||||
- TTS: add auto mode enum (off/always/inbound/tagged) with per-session `/tts` override. (#1667) Thanks @sebslight. https://docs.clawd.bot/tts
|
||||
- Dev: add prek pre-commit hooks + dependabot config for weekly updates. (#1720) Thanks @dguido.
|
||||
- Docs: expand FAQ (migration, scheduling, concurrency, model recommendations, OpenAI subscription auth, Pi sizing, hackable install, docs SSL workaround).
|
||||
- Docs: add verbose installer troubleshooting guidance.
|
||||
- Docs: add macOS VM guide with local/hosted options + VPS/nodes guidance. (#1693) Thanks @f-trycua.
|
||||
@@ -25,6 +26,7 @@ Docs: https://docs.clawd.bot
|
||||
- Diagnostics: add diagnostic flags for targeted debug logs (config + env override). https://docs.clawd.bot/diagnostics/flags
|
||||
|
||||
### Fixes
|
||||
- macOS: rearm gateway receive loop before push handling to avoid node invoke stalls. (#1752) Thanks @ngutman.
|
||||
- Gateway: include inline config env vars in service install environments. (#1735) Thanks @Seredeep.
|
||||
- BlueBubbles: route phone-number targets to DMs, avoid leaking routing IDs, and auto-create missing DMs (Private API required). (#1751) Thanks @tyler6204. https://docs.clawd.bot/channels/bluebubbles
|
||||
- BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing.
|
||||
|
||||
@@ -459,7 +459,7 @@ Use these when you’re past the onboarding flow and want the deeper reference.
|
||||
|
||||
## 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.
|
||||
|
||||
- [clawd.me](https://clawd.me)
|
||||
@@ -468,7 +468,7 @@ by Peter Steinberger and the 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! 🤖
|
||||
|
||||
Special thanks to [Mario Zechner](https://mariozechner.at/) for his support and for
|
||||
|
||||
@@ -12,4 +12,3 @@ If you believe you’ve found a security issue in Clawdbot, please report it pri
|
||||
For threat model + hardening guidance (including `clawdbot security audit --deep` and `--fix`), see:
|
||||
|
||||
- `https://docs.clawd.bot/gateway/security`
|
||||
|
||||
|
||||
@@ -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=="/>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
</rss>
|
||||
|
||||
@@ -12,4 +12,3 @@ data class CameraHudState(
|
||||
val kind: CameraHudKind,
|
||||
val message: String,
|
||||
)
|
||||
|
||||
|
||||
@@ -12,4 +12,3 @@ enum class VoiceWakeMode(val rawValue: String) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ class SmsManager(private val context: Context) {
|
||||
|
||||
/**
|
||||
* Send an SMS message.
|
||||
*
|
||||
*
|
||||
* @param paramsJson JSON with "to" (phone number) and "message" (text) fields
|
||||
* @return SendResult indicating success or failure
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#0A0A0A</color>
|
||||
</resources>
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">Clawdbot Node</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@@ -23,4 +23,3 @@ class VoiceWakeCommandExtractorTest {
|
||||
assertNull(VoiceWakeCommandExtractor.extractCommand("hey claude!", listOf("claude")))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,4 +16,3 @@ dependencyResolutionManagement {
|
||||
|
||||
rootProject.name = "ClawdbotNodeAndroid"
|
||||
include(":app")
|
||||
|
||||
|
||||
@@ -3,4 +3,3 @@ parent_config: ../../.swiftlint.yml
|
||||
included:
|
||||
- Sources
|
||||
- ../shared/ClawdisNodeKit/Sources
|
||||
|
||||
|
||||
@@ -33,4 +33,4 @@
|
||||
],
|
||||
"squares" : "shared"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
|
||||
@@ -173,4 +173,4 @@
|
||||
"iPod5,1": "iPod touch (5th generation)",
|
||||
"iPod7,1": "iPod touch (6th generation)",
|
||||
"iPod9,1": "iPod touch (7th generation)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,4 +211,4 @@
|
||||
"Mac Pro (2019)",
|
||||
"Mac Pro (Rack, 2019)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,8 +427,8 @@ public actor GatewayChannelActor {
|
||||
Task { await self.handleReceiveFailure(err) }
|
||||
case let .success(msg):
|
||||
Task {
|
||||
await self.handle(msg)
|
||||
await self.listen()
|
||||
await self.handle(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -574,46 +574,22 @@ public actor GatewayChannelActor {
|
||||
params: [String: AnyCodable]?,
|
||||
timeoutMs: Double? = nil) async throws -> Data
|
||||
{
|
||||
do {
|
||||
try await self.connect()
|
||||
} catch {
|
||||
throw self.wrap(error, context: "gateway connect")
|
||||
}
|
||||
let id = UUID().uuidString
|
||||
try await self.connectOrThrow(context: "gateway connect")
|
||||
let effectiveTimeout = timeoutMs ?? self.defaultRequestTimeoutMs
|
||||
// Encode request using the generated models to avoid JSONSerialization/ObjC bridging pitfalls.
|
||||
let paramsObject: ProtoAnyCodable? = params.map { entries in
|
||||
let dict = entries.reduce(into: [String: ProtoAnyCodable]()) { dict, entry in
|
||||
dict[entry.key] = ProtoAnyCodable(entry.value.value)
|
||||
}
|
||||
return ProtoAnyCodable(dict)
|
||||
}
|
||||
let frame = RequestFrame(
|
||||
type: "req",
|
||||
id: id,
|
||||
method: method,
|
||||
params: paramsObject)
|
||||
let data: Data
|
||||
do {
|
||||
data = try self.encoder.encode(frame)
|
||||
} catch {
|
||||
self.logger.error(
|
||||
"gateway request encode failed \(method, privacy: .public) error=\(error.localizedDescription, privacy: .public)")
|
||||
throw error
|
||||
}
|
||||
let payload = try self.encodeRequest(method: method, params: params, kind: "request")
|
||||
let response = try await withCheckedThrowingContinuation { (cont: CheckedContinuation<GatewayFrame, Error>) in
|
||||
self.pending[id] = cont
|
||||
self.pending[payload.id] = cont
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
try? await Task.sleep(nanoseconds: UInt64(effectiveTimeout * 1_000_000))
|
||||
await self.timeoutRequest(id: id, timeoutMs: effectiveTimeout)
|
||||
await self.timeoutRequest(id: payload.id, timeoutMs: effectiveTimeout)
|
||||
}
|
||||
Task {
|
||||
do {
|
||||
try await self.task?.send(.data(data))
|
||||
try await self.task?.send(.data(payload.data))
|
||||
} catch {
|
||||
let wrapped = self.wrap(error, context: "gateway send \(method)")
|
||||
let waiter = self.pending.removeValue(forKey: id)
|
||||
let waiter = self.pending.removeValue(forKey: payload.id)
|
||||
// Treat send failures as a broken socket: mark disconnected and trigger reconnect.
|
||||
self.connected = false
|
||||
self.task?.cancel(with: .goingAway, reason: nil)
|
||||
@@ -657,6 +633,42 @@ public actor GatewayChannelActor {
|
||||
return NSError(domain: ns.domain, code: ns.code, userInfo: [NSLocalizedDescriptionKey: "\(context): \(desc)"])
|
||||
}
|
||||
|
||||
private func connectOrThrow(context: String) async throws {
|
||||
do {
|
||||
try await self.connect()
|
||||
} catch {
|
||||
throw self.wrap(error, context: context)
|
||||
}
|
||||
}
|
||||
|
||||
private func encodeRequest(
|
||||
method: String,
|
||||
params: [String: AnyCodable]?,
|
||||
kind: String) throws -> (id: String, data: Data)
|
||||
{
|
||||
let id = UUID().uuidString
|
||||
// Encode request using the generated models to avoid JSONSerialization/ObjC bridging pitfalls.
|
||||
let paramsObject: ProtoAnyCodable? = params.map { entries in
|
||||
let dict = entries.reduce(into: [String: ProtoAnyCodable]()) { dict, entry in
|
||||
dict[entry.key] = ProtoAnyCodable(entry.value.value)
|
||||
}
|
||||
return ProtoAnyCodable(dict)
|
||||
}
|
||||
let frame = RequestFrame(
|
||||
type: "req",
|
||||
id: id,
|
||||
method: method,
|
||||
params: paramsObject)
|
||||
do {
|
||||
let data = try self.encoder.encode(frame)
|
||||
return (id: id, data: data)
|
||||
} catch {
|
||||
self.logger.error(
|
||||
"gateway \(kind) encode failed \(method, privacy: .public) error=\(error.localizedDescription, privacy: .public)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private func failPending(_ error: Error) async {
|
||||
let waiters = self.pending
|
||||
self.pending.removeAll()
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import ClawdbotKit
|
||||
import ClawdbotProtocol
|
||||
|
||||
private final class FakeWebSocketTask: WebSocketTasking, @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var queue: [URLSessionWebSocketTask.Message] = []
|
||||
private var pendingHandler: (@Sendable (Result<URLSessionWebSocketTask.Message, Error>) -> Void)?
|
||||
private var pendingContinuation: CheckedContinuation<URLSessionWebSocketTask.Message, Error>?
|
||||
private let encoder = JSONEncoder()
|
||||
private let decoder = JSONDecoder()
|
||||
|
||||
var state: URLSessionTask.State = .running
|
||||
|
||||
func resume() {}
|
||||
|
||||
func cancel(with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
|
||||
state = .canceling
|
||||
}
|
||||
|
||||
func send(_ message: URLSessionWebSocketTask.Message) async throws {
|
||||
guard case let .data(data) = message else { return }
|
||||
guard let frame = try? decoder.decode(RequestFrame.self, from: data) else { return }
|
||||
switch frame.method {
|
||||
case "connect":
|
||||
enqueueResponse(id: frame.id, payload: helloOkPayload())
|
||||
default:
|
||||
enqueueResponse(id: frame.id, payload: ["ok": true])
|
||||
}
|
||||
}
|
||||
|
||||
func receive() async throws -> URLSessionWebSocketTask.Message {
|
||||
try await withCheckedThrowingContinuation { cont in
|
||||
lock.lock()
|
||||
if !queue.isEmpty {
|
||||
let msg = queue.removeFirst()
|
||||
lock.unlock()
|
||||
cont.resume(returning: msg)
|
||||
return
|
||||
}
|
||||
pendingContinuation = cont
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func receive(
|
||||
completionHandler: @escaping @Sendable (Result<URLSessionWebSocketTask.Message, Error>) -> Void)
|
||||
{
|
||||
lock.lock()
|
||||
if !queue.isEmpty {
|
||||
let msg = queue.removeFirst()
|
||||
lock.unlock()
|
||||
completionHandler(.success(msg))
|
||||
return
|
||||
}
|
||||
pendingHandler = completionHandler
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func enqueue(_ message: URLSessionWebSocketTask.Message) {
|
||||
lock.lock()
|
||||
if let handler = pendingHandler {
|
||||
pendingHandler = nil
|
||||
lock.unlock()
|
||||
handler(.success(message))
|
||||
return
|
||||
}
|
||||
if let continuation = pendingContinuation {
|
||||
pendingContinuation = nil
|
||||
lock.unlock()
|
||||
continuation.resume(returning: message)
|
||||
return
|
||||
}
|
||||
queue.append(message)
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
private func enqueueResponse(id: String, payload: [String: Any]) {
|
||||
let response = ResponseFrame(
|
||||
type: "res",
|
||||
id: id,
|
||||
ok: true,
|
||||
payload: ClawdbotProtocol.AnyCodable(payload),
|
||||
error: nil)
|
||||
guard let data = try? encoder.encode(response) else { return }
|
||||
enqueue(.data(data))
|
||||
}
|
||||
|
||||
private func helloOkPayload() -> [String: Any] {
|
||||
[
|
||||
"type": "hello.ok",
|
||||
"protocol": 1,
|
||||
"server": [:],
|
||||
"features": [:],
|
||||
"snapshot": [
|
||||
"presence": [],
|
||||
"health": [:],
|
||||
"stateVersion": [
|
||||
"presence": 0,
|
||||
"health": 0,
|
||||
],
|
||||
"uptimeMs": 0,
|
||||
],
|
||||
"policy": [
|
||||
"tickIntervalMs": 1000,
|
||||
],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
private final class FakeWebSocketSession: WebSocketSessioning {
|
||||
let task: FakeWebSocketTask
|
||||
|
||||
init(task: FakeWebSocketTask) {
|
||||
self.task = task
|
||||
}
|
||||
|
||||
func makeWebSocketTask(url: URL) -> WebSocketTaskBox {
|
||||
WebSocketTaskBox(task: task)
|
||||
}
|
||||
}
|
||||
|
||||
private actor AsyncSignal {
|
||||
private var continuation: CheckedContinuation<Result<Void, Error>, Never>?
|
||||
private var stored: Result<Void, Error>?
|
||||
|
||||
func finish(_ result: Result<Void, Error>) {
|
||||
if let continuation {
|
||||
self.continuation = nil
|
||||
continuation.resume(returning: result)
|
||||
return
|
||||
}
|
||||
stored = result
|
||||
}
|
||||
|
||||
func wait() async throws {
|
||||
let result = await withCheckedContinuation { cont in
|
||||
if let stored {
|
||||
self.stored = nil
|
||||
cont.resume(returning: stored)
|
||||
return
|
||||
}
|
||||
continuation = cont
|
||||
}
|
||||
switch result {
|
||||
case .success:
|
||||
return
|
||||
case let .failure(error):
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum TestError: Error {
|
||||
case timeout
|
||||
}
|
||||
|
||||
struct GatewayChannelTests {
|
||||
@Test
|
||||
func listenRearmsBeforePushHandler() async throws {
|
||||
let task = FakeWebSocketTask()
|
||||
let session = FakeWebSocketSession(task: task)
|
||||
let signal = AsyncSignal()
|
||||
let url = URL(string: "ws://example.invalid")!
|
||||
final class ChannelBox { var channel: GatewayChannelActor? }
|
||||
let box = ChannelBox()
|
||||
|
||||
let channel = GatewayChannelActor(
|
||||
url: url,
|
||||
token: nil,
|
||||
session: WebSocketSessionBox(session: session),
|
||||
pushHandler: { push in
|
||||
guard case let .event(evt) = push, evt.event == "test.event" else { return }
|
||||
guard let channel = box.channel else { return }
|
||||
let params: [String: ClawdbotKit.AnyCodable] = [
|
||||
"event": ClawdbotKit.AnyCodable("test"),
|
||||
"payloadJSON": ClawdbotKit.AnyCodable(NSNull()),
|
||||
]
|
||||
do {
|
||||
_ = try await channel.request(method: "node.event", params: params, timeoutMs: 50)
|
||||
await signal.finish(.success(()))
|
||||
} catch {
|
||||
await signal.finish(.failure(error))
|
||||
}
|
||||
})
|
||||
box.channel = channel
|
||||
|
||||
let challenge = EventFrame(
|
||||
type: "event",
|
||||
event: "connect.challenge",
|
||||
payload: ClawdbotProtocol.AnyCodable(["nonce": "test-nonce"]),
|
||||
seq: nil,
|
||||
stateversion: nil)
|
||||
let encoder = JSONEncoder()
|
||||
task.enqueue(.data(try encoder.encode(challenge)))
|
||||
|
||||
try await channel.connect()
|
||||
|
||||
let event = EventFrame(
|
||||
type: "event",
|
||||
event: "test.event",
|
||||
payload: ClawdbotProtocol.AnyCodable([:]),
|
||||
seq: nil,
|
||||
stateversion: nil)
|
||||
task.enqueue(.data(try encoder.encode(event)))
|
||||
|
||||
try await AsyncTimeout.withTimeout(seconds: 1, onTimeout: { TestError.timeout }) {
|
||||
try await signal.wait()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,6 +138,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
||||
- [Can I use self-hosted models (llama.cpp, vLLM, Ollama)?](#can-i-use-selfhosted-models-llamacpp-vllm-ollama)
|
||||
- [What do Clawd, Flawd, and Krill use for models?](#what-do-clawd-flawd-and-krill-use-for-models)
|
||||
- [How do I switch models on the fly (without restarting)?](#how-do-i-switch-models-on-the-fly-without-restarting)
|
||||
- [Can I use GPT 5.2 for daily tasks and Codex 5.2 for coding](#can-i-use-gpt-52-for-daily-tasks-and-codex-52-for-coding)
|
||||
- [Why do I see “Model … is not allowed” and then no reply?](#why-do-i-see-model-is-not-allowed-and-then-no-reply)
|
||||
- [Why do I see “Unknown model: minimax/MiniMax-M2.1”?](#why-do-i-see-unknown-model-minimaxminimaxm21)
|
||||
- [Can I use MiniMax as my default and OpenAI for complex tasks?](#can-i-use-minimax-as-my-default-and-openai-for-complex-tasks)
|
||||
@@ -1947,6 +1948,16 @@ Re-run `/model` **without** the `@profile` suffix:
|
||||
If you want to return to the default, pick it from `/model` (or send `/model <default provider/model>`).
|
||||
Use `/model status` to confirm which auth profile is active.
|
||||
|
||||
### Can I use GPT 5.2 for daily tasks and Codex 5.2 for coding
|
||||
|
||||
Yes. Set one as default and switch as needed:
|
||||
|
||||
- **Quick switch (per session):** `/model gpt-5.2` for daily tasks, `/model gpt-5.2-codex` for coding.
|
||||
- **Default + switch:** set `agents.defaults.model.primary` to `openai-codex/gpt-5.2`, then switch to `openai-codex/gpt-5.2-codex` when coding (or the other way around).
|
||||
- **Sub-agents:** route coding tasks to sub-agents with a different default model.
|
||||
|
||||
See [Models](/concepts/models) and [Slash commands](/tools/slash-commands).
|
||||
|
||||
### Why do I see Model is not allowed and then no reply
|
||||
|
||||
If `agents.defaults.models` is set, it becomes the **allowlist** for `/model` and any
|
||||
|
||||
@@ -33,7 +33,7 @@ export function registerMatrixAutoJoin(params: {
|
||||
// For "allowlist" mode, handle invites manually
|
||||
client.on("room.invite", async (roomId: string, _inviteEvent: unknown) => {
|
||||
if (autoJoin !== "allowlist") return;
|
||||
|
||||
|
||||
// Get room alias if available
|
||||
let alias: string | undefined;
|
||||
let altAliases: string[] = [];
|
||||
|
||||
@@ -25,7 +25,7 @@ async function fetchMatrixMediaBuffer(params: {
|
||||
// matrix-bot-sdk provides mxcToHttp helper
|
||||
const url = params.client.mxcToHttp(params.mxcUrl);
|
||||
if (!url) return null;
|
||||
|
||||
|
||||
// Use the client's download method which handles auth
|
||||
try {
|
||||
const buffer = await params.client.downloadContent(params.mxcUrl);
|
||||
@@ -61,7 +61,7 @@ async function fetchEncryptedMediaBuffer(params: {
|
||||
Buffer.from(encryptedBuffer),
|
||||
params.file,
|
||||
);
|
||||
|
||||
|
||||
return { buffer: decrypted };
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export async function downloadMatrixMedia(params: {
|
||||
placeholder: string;
|
||||
} | null> {
|
||||
let fetched: { buffer: Buffer; headerType?: string } | null;
|
||||
|
||||
|
||||
if (params.file) {
|
||||
// Encrypted media
|
||||
fetched = await fetchEncryptedMediaBuffer({
|
||||
@@ -93,7 +93,7 @@ export async function downloadMatrixMedia(params: {
|
||||
maxBytes: params.maxBytes,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (!fetched) return null;
|
||||
const headerType = fetched.headerType ?? params.contentType ?? undefined;
|
||||
const saved = await getMatrixRuntime().channel.media.saveMediaBuffer(
|
||||
|
||||
@@ -11,4 +11,4 @@ export function resolveMattermostGroupRequireMention(
|
||||
});
|
||||
if (typeof account.requireMention === "boolean") return account.requireMention;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,4 +112,4 @@ export function listEnabledMattermostAccounts(cfg: ClawdbotConfig): ResolvedMatt
|
||||
return listMattermostAccountIds(cfg)
|
||||
.map((accountId) => resolveMattermostAccount({ cfg, accountId }))
|
||||
.filter((account) => account.enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,4 +205,4 @@ export async function uploadMattermostFile(
|
||||
throw new Error("Mattermost file upload failed");
|
||||
}
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,4 +147,4 @@ export function resolveThreadSessionKeys(params: {
|
||||
? `${params.baseSessionKey}:thread:${threadId}`
|
||||
: params.baseSessionKey;
|
||||
return { sessionKey, parentSessionKey: params.parentSessionKey };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,4 +67,4 @@ export async function probeMattermost(
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,4 +39,4 @@ export async function promptAccountId(params: PromptAccountIdParams): Promise<st
|
||||
);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,4 +184,4 @@ export const mattermostOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
mattermost: { ...cfg.channels?.mattermost, enabled: false },
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -22,11 +22,11 @@ parallel:
|
||||
security = session: security_expert
|
||||
prompt: "Perform a deep security audit of the changes. Look for OWASP top 10 issues."
|
||||
context: overview
|
||||
|
||||
|
||||
perf = session: performance_expert
|
||||
prompt: "Analyze the performance implications. Identify potential bottlenecks or regressions."
|
||||
context: overview
|
||||
|
||||
|
||||
style = session: reviewer
|
||||
prompt: "Review for code style, maintainability, and adherence to best practices."
|
||||
context: overview
|
||||
|
||||
@@ -19,4 +19,3 @@ export type CallManagerContext = {
|
||||
transcriptWaiters: Map<CallId, TranscriptWaiter>;
|
||||
maxDurationTimers: Map<CallId, NodeJS.Timeout>;
|
||||
};
|
||||
|
||||
|
||||
@@ -175,4 +175,3 @@ export function processEvent(ctx: CallManagerContext, event: NormalizedEvent): v
|
||||
|
||||
persistCallRecord(ctx.storePath, call);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,4 +31,3 @@ export function findCall(params: {
|
||||
providerCallId: params.callIdOrProviderCallId,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -48,4 +48,3 @@ export function addTranscriptEntry(
|
||||
};
|
||||
call.transcript.push(entry);
|
||||
}
|
||||
|
||||
|
||||
@@ -86,4 +86,3 @@ export async function getCallHistoryFromStore(
|
||||
|
||||
return calls;
|
||||
}
|
||||
|
||||
|
||||
@@ -84,4 +84,3 @@ export function waitForFinalTranscript(
|
||||
ctx.transcriptWaiters.set(callId, { resolve, reject, timeout });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,4 +7,3 @@ export function generateNotifyTwiml(message: string, voice: string): string {
|
||||
<Hangup/>
|
||||
</Response>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,4 +26,3 @@ describe("PlivoProvider", () => {
|
||||
expect(result.providerResponseBody).toContain('length="300"');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -27,4 +27,3 @@ export function verifyTwilioProviderWebhook(params: {
|
||||
reason: result.reason,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -15,4 +15,3 @@ describe("zalouser outbound chunker", () => {
|
||||
expect(chunks.every((c) => c.length <= limit)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ EOF
|
||||
# Function to list categories
|
||||
list_categories() {
|
||||
echo -e "${BLUE}Fetching VibeTunnel log categories from the last hour...${NC}\n"
|
||||
|
||||
|
||||
# Get unique categories from recent logs
|
||||
log show --predicate "subsystem == \"$SUBSYSTEM\"" --last 1h 2>/dev/null | \
|
||||
grep -E "category: \"[^\"]+\"" | \
|
||||
@@ -133,7 +133,7 @@ list_categories() {
|
||||
while read -r cat; do
|
||||
echo " • $cat"
|
||||
done
|
||||
|
||||
|
||||
echo -e "\n${YELLOW}Note: Only categories with recent activity are shown${NC}"
|
||||
}
|
||||
|
||||
@@ -230,29 +230,29 @@ fi
|
||||
if [[ "$STREAM_MODE" == true ]]; then
|
||||
# Streaming mode
|
||||
CMD="sudo log stream --predicate '$PREDICATE' --level $LOG_LEVEL --info"
|
||||
|
||||
|
||||
echo -e "${GREEN}Streaming VibeTunnel logs continuously...${NC}"
|
||||
echo -e "${YELLOW}Press Ctrl+C to stop${NC}\n"
|
||||
else
|
||||
# Show mode
|
||||
CMD="sudo log show --predicate '$PREDICATE'"
|
||||
|
||||
|
||||
# Add log level for show command
|
||||
if [[ "$LOG_LEVEL" == "debug" ]]; then
|
||||
CMD="$CMD --debug"
|
||||
else
|
||||
CMD="$CMD --info"
|
||||
fi
|
||||
|
||||
|
||||
# Add time range
|
||||
CMD="$CMD --last $TIME_RANGE"
|
||||
|
||||
|
||||
if [[ "$SHOW_TAIL" == true ]]; then
|
||||
echo -e "${GREEN}Showing last $TAIL_LINES log lines from the past $TIME_RANGE${NC}"
|
||||
else
|
||||
echo -e "${GREEN}Showing all logs from the past $TIME_RANGE${NC}"
|
||||
fi
|
||||
|
||||
|
||||
# Show applied filters
|
||||
if [[ "$ERRORS_ONLY" == true ]]; then
|
||||
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
|
||||
handle_sudo_error
|
||||
fi
|
||||
|
||||
|
||||
echo -e "${BLUE}Exporting logs to: $OUTPUT_FILE${NC}\n"
|
||||
if [[ "$SHOW_TAIL" == true ]] && [[ "$STREAM_MODE" == false ]]; then
|
||||
eval "$CMD" 2>&1 | tail -n "$TAIL_LINES" > "$OUTPUT_FILE"
|
||||
else
|
||||
eval "$CMD" > "$OUTPUT_FILE" 2>&1
|
||||
fi
|
||||
|
||||
|
||||
# Check if file was created and has content
|
||||
if [[ -s "$OUTPUT_FILE" ]]; then
|
||||
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
|
||||
handle_sudo_error
|
||||
fi
|
||||
|
||||
|
||||
if [[ "$SHOW_TAIL" == true ]] && [[ "$STREAM_MODE" == false ]]; then
|
||||
# Apply tail for non-streaming mode
|
||||
eval "$CMD" 2>&1 | tail -n "$TAIL_LINES"
|
||||
|
||||
@@ -102,12 +102,12 @@ ws.send(
|
||||
);
|
||||
const connectRes = await onceFrame((o) => o?.type === \"res\" && o?.id === \"c1\");
|
||||
if (!connectRes.ok) throw new Error(\"connect failed: \" + (connectRes.error?.message ?? \"unknown\"));
|
||||
|
||||
|
||||
ws.send(JSON.stringify({ type: \"req\", id: \"h1\", method: \"health\" }));
|
||||
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.payload?.ok !== true) throw new Error(\"unexpected health payload\");
|
||||
|
||||
|
||||
ws.close();
|
||||
console.log(\"ok\");
|
||||
NODE"
|
||||
|
||||
31
scripts/pre-commit/run-node-tool.sh
Executable file
31
scripts/pre-commit/run-node-tool.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "usage: run-node-tool.sh <tool> [args...]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
tool="$1"
|
||||
shift
|
||||
|
||||
if [[ -f "$ROOT_DIR/pnpm-lock.yaml" ]] && command -v pnpm >/dev/null 2>&1; then
|
||||
exec pnpm exec "$tool" "$@"
|
||||
fi
|
||||
|
||||
if { [[ -f "$ROOT_DIR/bun.lockb" ]] || [[ -f "$ROOT_DIR/bun.lock" ]]; } && command -v bun >/dev/null 2>&1; then
|
||||
exec bunx --bun "$tool" "$@"
|
||||
fi
|
||||
|
||||
if command -v npm >/dev/null 2>&1; then
|
||||
exec npm exec -- "$tool" "$@"
|
||||
fi
|
||||
|
||||
if command -v npx >/dev/null 2>&1; then
|
||||
exec npx "$tool" "$@"
|
||||
fi
|
||||
|
||||
echo "Missing package manager: pnpm, bun, or npm required." >&2
|
||||
exit 1
|
||||
@@ -30,4 +30,3 @@ export type Entry = {
|
||||
avatar_url: string;
|
||||
lines: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ curl http://127.0.0.1:8000/places/{place_id}
|
||||
"open_now": true
|
||||
}
|
||||
],
|
||||
"next_page_token": "..."
|
||||
"next_page_token": "..."
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
import "./styles.css";
|
||||
import "./ui/app.ts";
|
||||
|
||||
|
||||
@@ -279,4 +279,3 @@
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -122,4 +122,3 @@
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -196,4 +196,3 @@
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -242,8 +242,10 @@
|
||||
.config-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
background: var(--panel);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Actions Bar */
|
||||
|
||||
@@ -3,4 +3,3 @@ export type EventLogEntry = {
|
||||
event: string;
|
||||
payload?: unknown;
|
||||
};
|
||||
|
||||
|
||||
@@ -154,13 +154,13 @@ const COMPACTION_TOAST_DURATION_MS = 5000;
|
||||
export function handleCompactionEvent(host: CompactionHost, payload: AgentEventPayload) {
|
||||
const data = payload.data ?? {};
|
||||
const phase = typeof data.phase === "string" ? data.phase : "";
|
||||
|
||||
|
||||
// Clear any existing timer
|
||||
if (host.compactionClearTimer != null) {
|
||||
window.clearTimeout(host.compactionClearTimer);
|
||||
host.compactionClearTimer = null;
|
||||
}
|
||||
|
||||
|
||||
if (phase === "start") {
|
||||
host.compactionStatus = {
|
||||
active: true,
|
||||
@@ -183,13 +183,13 @@ export function handleCompactionEvent(host: CompactionHost, payload: AgentEventP
|
||||
|
||||
export function handleAgentEvent(host: ToolStreamHost, payload?: AgentEventPayload) {
|
||||
if (!payload) return;
|
||||
|
||||
|
||||
// Handle compaction events
|
||||
if (payload.stream === "compaction") {
|
||||
handleCompactionEvent(host as CompactionHost, payload);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (payload.stream !== "tool") return;
|
||||
const sessionKey =
|
||||
typeof payload.sessionKey === "string" ? payload.sessionKey : undefined;
|
||||
|
||||
@@ -74,4 +74,3 @@ export function removePathValue(
|
||||
delete (current as Record<string, unknown>)[lastKey];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,4 +54,3 @@ export async function callDebugMethod(state: DebugState) {
|
||||
state.debugCallError = String(err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,4 +33,3 @@ export async function loadPresence(state: PresenceState) {
|
||||
state.presenceLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,4 +39,3 @@ describe("stripThinkingTags", () => {
|
||||
expect(stripThinkingTags("Hello</final>")).toBe("Hello");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -30,4 +30,3 @@ describe("toSanitizedMarkdownHtml", () => {
|
||||
expect(html).toContain("console.log(1)");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -55,4 +55,3 @@ export function formatCronPayload(job: CronJob) {
|
||||
if (p.kind === "systemEvent") return `System: ${p.text}`;
|
||||
return `Agent: ${p.message}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}$/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -40,4 +40,3 @@ export function generateUUID(cryptoLike: CryptoLike | null = globalThis.crypto):
|
||||
|
||||
return uuidFromBytes(weakRandomBytes());
|
||||
}
|
||||
|
||||
|
||||
@@ -43,4 +43,3 @@ export function renderChannelAccountCount(
|
||||
if (count < 2) return nothing;
|
||||
return html`<div class="account-count">Accounts (${count})</div>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -116,4 +116,3 @@ export function renderWhatsAppCard(params: {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ const COMPACTION_TOAST_DURATION_MS = 5000;
|
||||
|
||||
function renderCompactionIndicator(status: CompactionIndicatorStatus | null | undefined) {
|
||||
if (!status) return nothing;
|
||||
|
||||
|
||||
// Show "compacting..." while active
|
||||
if (status.active) {
|
||||
return html`
|
||||
@@ -91,7 +91,7 @@ function renderCompactionIndicator(status: CompactionIndicatorStatus | null | un
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nothing;
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ export function renderNode(params: {
|
||||
const hasString = normalizedTypes.has("string");
|
||||
const hasNumber = normalizedTypes.has("number");
|
||||
const hasBoolean = normalizedTypes.has("boolean");
|
||||
|
||||
|
||||
if (hasBoolean && normalizedTypes.size === 1) {
|
||||
return renderNode({
|
||||
...params,
|
||||
@@ -383,14 +383,14 @@ function renderObject(params: {
|
||||
const hint = hintForPath(path, hints);
|
||||
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
|
||||
const help = hint?.help ?? schema.description;
|
||||
|
||||
|
||||
const fallback = value ?? schema.default;
|
||||
const obj = fallback && typeof fallback === "object" && !Array.isArray(fallback)
|
||||
? (fallback as Record<string, unknown>)
|
||||
: {};
|
||||
const props = schema.properties ?? {};
|
||||
const entries = Object.entries(props);
|
||||
|
||||
|
||||
// Sort by hint order
|
||||
const sorted = entries.sort((a, b) => {
|
||||
const orderA = hintForPath([...path, a[0]], hints)?.order ?? 0;
|
||||
@@ -514,7 +514,7 @@ function renderArray(params: {
|
||||
</button>
|
||||
</div>
|
||||
${help ? html`<div class="cfg-array__help">${help}</div>` : nothing}
|
||||
|
||||
|
||||
${arr.length === 0 ? html`
|
||||
<div class="cfg-array__empty">
|
||||
No items yet. Click "Add" to create one.
|
||||
@@ -597,7 +597,7 @@ function renderMapField(params: {
|
||||
Add Entry
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
${entries.length === 0 ? html`
|
||||
<div class="cfg-map__empty">No custom entries.</div>
|
||||
` : html`
|
||||
|
||||
@@ -94,16 +94,16 @@ function matchesSearch(key: string, schema: JsonSchema, query: string): boolean
|
||||
if (!query) return true;
|
||||
const q = query.toLowerCase();
|
||||
const meta = SECTION_META[key];
|
||||
|
||||
|
||||
// Check key name
|
||||
if (key.toLowerCase().includes(q)) return true;
|
||||
|
||||
|
||||
// Check label and description
|
||||
if (meta) {
|
||||
if (meta.label.toLowerCase().includes(q)) return true;
|
||||
if (meta.description.toLowerCase().includes(q)) return true;
|
||||
}
|
||||
|
||||
|
||||
return schemaMatches(schema, q);
|
||||
}
|
||||
|
||||
@@ -192,8 +192,8 @@ export function renderConfigForm(props: ConfigFormProps) {
|
||||
<div class="config-empty">
|
||||
<div class="config-empty__icon">${icons.search}</div>
|
||||
<div class="config-empty__text">
|
||||
${searchQuery
|
||||
? `No settings match "${searchQuery}"`
|
||||
${searchQuery
|
||||
? `No settings match "${searchQuery}"`
|
||||
: "No settings in this section"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -89,4 +89,3 @@ export function isSensitivePath(path: Array<string | number>): boolean {
|
||||
key.endsWith("key")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,4 +5,3 @@ export {
|
||||
} from "./config-form.analyze";
|
||||
export { renderNode } from "./config-form.node";
|
||||
export { schemaType, type JsonSchema } from "./config-form.shared";
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ function computeDiff(
|
||||
): Array<{ path: string; from: unknown; to: unknown }> {
|
||||
if (!original || !current) return [];
|
||||
const changes: Array<{ path: string; from: unknown; to: unknown }> = [];
|
||||
|
||||
|
||||
function compare(orig: unknown, curr: unknown, path: string) {
|
||||
if (orig === curr) return;
|
||||
if (typeof orig !== typeof curr) {
|
||||
@@ -164,7 +164,7 @@ function computeDiff(
|
||||
compare(origObj[key], currObj[key], path ? `${path}.${key}` : key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
compare(original, current, "");
|
||||
return changes;
|
||||
}
|
||||
@@ -258,7 +258,7 @@ export function renderConfig(props: ConfigProps) {
|
||||
<div class="config-sidebar__title">Settings</div>
|
||||
<span class="pill pill--sm ${validity === "valid" ? "pill--ok" : validity === "invalid" ? "pill--danger" : ""}">${validity}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Search -->
|
||||
<div class="config-search">
|
||||
<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)}
|
||||
/>
|
||||
${props.searchQuery ? html`
|
||||
<button
|
||||
<button
|
||||
class="config-search__clear"
|
||||
@click=${() => props.onSearchChange("")}
|
||||
>×</button>
|
||||
` : nothing}
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Section nav -->
|
||||
<nav class="config-nav">
|
||||
<button
|
||||
@@ -299,7 +299,7 @@ export function renderConfig(props: ConfigProps) {
|
||||
</button>
|
||||
`)}
|
||||
</nav>
|
||||
|
||||
|
||||
<!-- Mode toggle at bottom -->
|
||||
<div class="config-sidebar__footer">
|
||||
<div class="config-mode-toggle">
|
||||
@@ -319,7 +319,7 @@ export function renderConfig(props: ConfigProps) {
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="config-main">
|
||||
<!-- Action bar -->
|
||||
@@ -358,7 +358,7 @@ export function renderConfig(props: ConfigProps) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Diff panel (form mode only - raw mode doesn't have granular diff) -->
|
||||
${hasChanges && props.formMode === "form" ? html`
|
||||
<details class="config-diff">
|
||||
|
||||
17
zizmor.yml
Normal file
17
zizmor.yml
Normal 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
|
||||
Reference in New Issue
Block a user