@swarmclawai/swarmclaw 1.5.56 → 1.5.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -102
- package/package.json +1 -1
- package/src/app/api/chatrooms/refusal-policy/route.ts +43 -0
- package/src/app/api/config-versions/restore/route.ts +26 -0
- package/src/app/api/config-versions/route.ts +21 -0
- package/src/app/api/eval/scenarios/route.ts +6 -4
- package/src/app/api/eval/suite/route.ts +5 -1
- package/src/app/api/eval/suites/route.ts +19 -0
- package/src/app/api/task-workflow-states/route.ts +69 -0
- package/src/app/api/usage/by-code/route.ts +32 -0
- package/src/app/api/usage/live/route.ts +94 -0
- package/src/app/api/workspaces/active/route.ts +19 -0
- package/src/app/api/workspaces/route.ts +46 -0
- package/src/cli/index.js +47 -1
- package/src/cli/spec.js +39 -0
- package/src/components/auth/setup-wizard/utils.test.ts +8 -1
- package/src/components/auth/setup-wizard/utils.ts +8 -1
- package/src/lib/server/agents/agent-budget-hook.ts +29 -0
- package/src/lib/server/agents/agent-service.ts +16 -0
- package/src/lib/server/agents/main-agent-loop.ts +8 -0
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +35 -6
- package/src/lib/server/chat-execution/post-stream-finalization.ts +2 -0
- package/src/lib/server/chatrooms/chatroom-refusal.ts +108 -0
- package/src/lib/server/config-versions/config-version-repository.ts +116 -0
- package/src/lib/server/eval/runner.ts +16 -5
- package/src/lib/server/eval/scenarios-gaia.ts +100 -0
- package/src/lib/server/eval/scenarios-swebench.ts +196 -0
- package/src/lib/server/eval/scenarios.ts +20 -1
- package/src/lib/server/eval/types.ts +4 -0
- package/src/lib/server/missions/mission-templates.ts +23 -0
- package/src/lib/server/portability/export.ts +201 -5
- package/src/lib/server/portability/import.ts +214 -23
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +8 -2
- package/src/lib/server/tasks/workflow-state-repository.ts +98 -0
- package/src/lib/server/usage/cost-attribution.ts +85 -0
- package/src/lib/server/usage/resolve-billing-codes.ts +22 -0
- package/src/lib/server/workspaces/workspace-registry.ts +143 -0
- package/src/lib/setup-defaults.ts +76 -0
- package/src/types/config-version.ts +20 -0
- package/src/types/misc.ts +12 -0
- package/src/types/mission.ts +4 -0
- package/src/types/session.ts +2 -0
- package/src/types/task.ts +4 -0
- package/src/types/workflow-state.ts +41 -0
- package/src/types/workspace.ts +27 -0
package/README.md
CHANGED
|
@@ -399,6 +399,30 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
399
399
|
|
|
400
400
|
## Releases
|
|
401
401
|
|
|
402
|
+
### v1.5.58 Highlights
|
|
403
|
+
|
|
404
|
+
This release broadens the built-in evaluation harness so SwarmClaw runs can be benchmarked against named suites, adds two targeted starter kits, exposes live per-session cost data, tightens auto-skill drafting, and ships a zero-setup demo mission template.
|
|
405
|
+
|
|
406
|
+
- **Benchmark-style eval suites.** New `SWEBENCH_LITE_SCENARIOS` and `GAIA_L1_SCENARIOS` in `src/lib/server/eval/scenarios-swebench.ts` and `scenarios-gaia.ts` — curated parallels (not the upstream datasets) sized for a single-agent harness run. The shared `EvalScenario` type now carries an optional `suite: 'core' | 'swe-bench-lite' | 'gaia-l1' | 'tool-use' | 'code-action'` tag. `POST /api/eval/suite` accepts `{ suite: "swe-bench-lite" }` to scope a run. New `GET /api/eval/suites` lists every suite with scenario count, max score, and categories. CLI commands: `swarmclaw eval suites`, and `swarmclaw eval suite` still takes a JSON body now including `suite`. Useful for advertising verifiable numbers against a named benchmark instead of a bespoke scoring rubric.
|
|
407
|
+
- **Two additional starter kits.** `inbox_triage` (single Triager agent over email + memory + documents) and `data_analyst` (single Analyst agent over shell + files + web + documents) join the existing seven kits in `src/lib/setup-defaults.ts`. Both are surfaced on the intent-driven setup path alongside Personal Assistant, Research Copilot, Builder Studio, and Delegate Team.
|
|
408
|
+
- **Live per-session usage API.** New `GET /api/usage/live?sessionId=...` returns a lightweight snapshot — records, tokens in/out, estimated cost, firstAt/lastAt, wallclockMs, turns — so frontends can surface a live cost meter without pulling the full aggregated `/api/usage` payload. Without a `sessionId` the route returns the ten most recently active sessions. Registered in the CLI as `swarmclaw usage live`.
|
|
409
|
+
- **Auto-skill drafting is stricter and rate-limited.** `shouldAutoDraftSkillSuggestion` in `chat-turn-finalization.ts` now requires at least 3 tool events in the completed turn (was 1), and a new per-agent daily cap limits automatic drafts to 3 per day per agent to prevent suggestion-inbox spam. Both thresholds are named constants (`AUTO_DRAFT_MIN_TOOL_EVENTS`, `AUTO_DRAFT_DAILY_LIMIT`). Agents with `autoDraftSkillSuggestions = false` are unaffected (auto-drafting remains opt-in per agent).
|
|
410
|
+
- **Hello World demo mission template.** New `hello-world-demo` entry in `BUILT_IN_MISSION_TEMPLATES` — a bounded, zero-setup mission that reads three files in the working directory and writes a one-paragraph markdown summary to `hello-world-report.md`. Budgets (USD 0.25, 20k tokens, 30 turns, 15 min) are small enough to run on a local Ollama model without cost. Intended as the first thing a new user watches an agent complete end to end.
|
|
411
|
+
|
|
412
|
+
### v1.5.57 Highlights
|
|
413
|
+
|
|
414
|
+
This release closes the org-orchestration feature gap with Paperclip while keeping SwarmClaw's autonomous-assistant focus. Most additions are additive; nothing existing has changed shape.
|
|
415
|
+
|
|
416
|
+
- **Workspace templates: full export/import bundle.** `src/lib/server/portability/{export,import}.ts` now round-trips agents, skills, schedules, **connectors** (with secret scrubbing), **chatrooms**, **MCP servers**, **projects**, **goals**, and an extension manifest reference. Manifest version bumped to `2`; v1 bundles still import. Connectors and MCP servers re-import with credentials stripped — the response payload now lists which records `needCredentials` so the UI can prompt. ID remapping handles cross-references (chatrooms → agents, schedules → agents, goals → projects/agents).
|
|
417
|
+
- **Per-agent budget enforcement at enqueue.** New `src/lib/server/agents/agent-budget-hook.ts` mirrors the existing mission budget hook. When an agent has `budgetAction: 'block'` and any window (`hourlyBudget`/`dailyBudget`/`monthlyBudget`) is exhausted, autonomous enqueues now fail fast in `session-run-manager` instead of getting blocked deeper in the chat-turn pipeline. User-initiated chats still flow through (so users can talk to an agent that's hit its cap). Default `'warn'` behavior is unchanged.
|
|
418
|
+
- **Goal hierarchy ancestry through Mission.** `Mission.goalId` is a new optional field. When a session has a `missionId` and the bound mission has a `goalId`, `main-agent-loop.ts` now walks `mission → goal → parentGoal → …` so the full Initiative/Project/Goal ancestry flows into the agent system prompt — previously only direct session-level goals were resolved.
|
|
419
|
+
- **Billing codes / cost attribution.** `Mission`, `BoardTask`, `Session`, and `UsageRecord` accept an optional `billingCodes: string[]`. `resolveBillingCodesForSession` combines session + mission codes when usage is appended, and the new `GET /api/usage/by-code?codes=foo,bar&range=7d` endpoint rolls up cost per code (and per agent within each code). Lets users running multiple parallel projects answer "what did Project X cost?" across agents, missions, and ad-hoc chats.
|
|
420
|
+
- **Customizable task workflow states.** New `WorkflowState` collection (`src/lib/server/tasks/workflow-state-repository.ts`) stores team-defined states like "Needs Review" or "Blocked on PM" that are orthogonal to `BoardTaskStatus` lifecycle. `BoardTask.workflowStateId` references one of seven defaults (Triage / Backlog / Todo / In Progress / Needs Review / Done / Cancelled) or any custom state. CRUD via `GET|POST|DELETE /api/task-workflow-states`. Atomic checkout via `task-checkout.ts` was already in place.
|
|
421
|
+
- **Cross-agent delegation refusal policy.** `Chatroom.onRefusal` (`'reroute' | 'escalate' | 'human'`) and `Chatroom.escalationTargetAgentId` formalize what happens when a delegated agent declines work. `chatroom-refusal.ts` reroutes to another room member, escalates to a configured target, or surfaces a `human_loop` approval. Policy management at `POST /api/chatrooms/refusal-policy`; simulation at `PUT` for tests.
|
|
422
|
+
- **Configuration version history.** Every `updateAgent` call now snapshots the prior agent state into `config-versions.json` (capped at 50 versions per entity). `GET /api/config-versions?entityKind=agent&entityId=...` lists history; `POST /api/config-versions/restore` rolls back. Foundation for extending to extensions, connectors, MCP servers, chatrooms, and projects.
|
|
423
|
+
- **Multi-workspace scaffolding.** New `Workspace` registry with `GET|POST|PATCH|DELETE /api/workspaces` and `GET|POST /api/workspaces/active`. The default workspace seeds itself on first read; switching the active workspace persists to `workspace-registry.json`. **Note:** this is metadata only in v1.5.57 — actual data-dir forking per workspace is intentionally deferred (low-risk shipping).
|
|
424
|
+
- **CLI manifest expanded.** New top-level groups: `workspaces`, `workflow-states`, `config-versions`, `cost-attribution`, `chatroom-policy`. Run `swarmclaw workspaces list`, `swarmclaw cost-attribution by-code --query codes=client-a,range=30d`, `swarmclaw config-versions list --query entityKind=agent,entityId=...`, etc. CLI route-coverage test passes.
|
|
425
|
+
|
|
402
426
|
### v1.5.56 Highlights
|
|
403
427
|
|
|
404
428
|
- **Fix: TTS error responses are now proper JSON instead of a raw Buffer blob.** `POST /api/tts` and `POST /api/tts/stream` previously returned `500` with the error message wrapped in a `new NextResponse(string, ...)` that the CLI JSON-decoded into `{"type":"Buffer","data":[78,111,...]}`. Both routes now return `NextResponse.json({error}, {status: 500})`. Regression test added.
|
|
@@ -422,108 +446,6 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
422
446
|
|
|
423
447
|
> **Note:** v1.5.53 release notes described the mission templates library, but the feature commit landed after the v1.5.53 tag was cut. v1.5.54 is the release that actually ships it.
|
|
424
448
|
|
|
425
|
-
### v1.5.53 Highlights
|
|
426
|
-
|
|
427
|
-
- **Fix: switching a session's model now sticks in the UI** ([#50](https://github.com/swarmclawai/swarmclaw/pull/50), thanks to [@borislavnnikolov](https://github.com/borislavnnikolov)). The **Switch Model** panel in the agent inspector was reading from `agent.provider` / `agent.model` (the agent's defaults) instead of `session.provider` / `session.model`, so after saving a model switch the collapsed pill still showed the agent default, the combobox reset to the default when reopened, and `selectedProvider` reverted on every save. `ModelSwitcherInline` now uses `session.provider || agent.provider` and `session.model || agent.model` as the source of truth, and its `useEffect` syncs to `session.provider` changes so a successful save updates the panel immediately.
|
|
428
|
-
|
|
429
|
-
### v1.5.52 Highlights
|
|
430
|
-
|
|
431
|
-
- **Session X-Ray now surfaces the backend execution log** ([#48](https://github.com/swarmclawai/swarmclaw/pull/48), thanks to [@borislavnnikolov](https://github.com/borislavnnikolov)). The debug panel fetches entries from the SQLite execution log on open and merges them with in-memory message events, sorted by time. Expandable entries show provider, model, stream errors, duration, and token counts — the info that was previously invisible when Ollama or other local-model runs failed silently. A new **Tools** filter tab, an `exec` badge for log-sourced entries, an entry count in the stats bar, and a Refresh button round it out. New API route `GET /api/chats/:id/execution-log` with `limit`, `since`, and `category` query params, registered in the CLI manifest as `swarmclaw chats execution-log`.
|
|
432
|
-
- **Execution errors now captured in the log** ([#48](https://github.com/swarmclawai/swarmclaw/pull/48)). `finalizeChatTurn()` writes a structured `error` entry to the execution log on terminal failure, recording provider, model, stream errors, duration, token counts, and whether a response was produced — so the Session X-Ray above actually has something to show.
|
|
433
|
-
- **Fix: blank task-sheet no longer shows `"null"` under *Blocked By*** ([#47](https://github.com/swarmclawai/swarmclaw/pull/47), thanks to [@borislavnnikolov](https://github.com/borislavnnikolov)). A successful task create/update returns `error: null`, and the old `'error' in res` guard treated that as a truthy error and rendered `String(null)` as a red "null" string under the Blocked By field. Now only non-empty string errors trigger the UI, and `depError` is cleared on dialog close so stale state cannot leak across re-opens.
|
|
434
|
-
|
|
435
|
-
### v1.5.51 Highlights
|
|
436
|
-
|
|
437
|
-
- **Desktop app now actually opens and renders on macOS**: packaged builds were broken in v1.5.50 by a stack of independent issues that each masked the next. This release unblocks the cold-boot path end to end. Measured cold-boot time on a populated install: ~1 second to first `/api/healthz` response, down from a hard 60-second timeout.
|
|
438
|
-
- Ad-hoc code signing (`identity: '-'`) via a new `scripts/electron-after-pack.cjs` hook that runs `codesign --sign - --force --deep` after electron-builder packages the bundle. The bundle identifier is now sealed as `ai.swarmclaw.desktop` with all 74k resources sealed, so quarantined dmgs surface as "unidentified developer" (right-click → Open) instead of the more confusing "damaged" error.
|
|
439
|
-
- Per-architecture native module sync: the afterPack hook copies `better-sqlite3`, `@mongodb-js/zstd`, `node-liblzma`, and `utf-8-validate` `.node` binaries from the electron-builder-rebuilt root `node_modules` into the packaged `.next/standalone/node_modules`. Without this, the standalone server hit `ERR_DLOPEN_FAILED: NODE_MODULE_VERSION 137` on launch because Next.js's output-tracing copied the Node-ABI build of better-sqlite3 into standalone while electron-builder only rebuilt the root tree for Electron's ABI.
|
|
440
|
-
- `scripts/run-next-build.mjs` now copies `mdn-data` (used by `css-tree` via `jsdom`) into standalone alongside the existing `css-tree/data` patch, so pages that depend on it don't 500 with `Cannot find module 'mdn-data/css/at-rules.json'`.
|
|
441
|
-
- `isomorphic-dompurify` replaced by the browser-only `dompurify` in `agent-avatar.tsx`. The isomorphic wrapper was pulling `jsdom`'s ESM-only `@exodus/bytes` dep into every server bundle the avatar was referenced from, which blew up SSR under Electron 33 (Node 20.18) with `ERR_REQUIRE_ESM` on every page.
|
|
442
|
-
- Session-consolidation migrations, `initWsServer`, and `ensureDaemonStarted` moved into a `setImmediate` deferred block in `src/instrumentation.ts` so Next.js can bind the HTTP listener before per-install work runs.
|
|
443
|
-
- **App icon fixed**: the Dock no longer shows Electron's default `exec` placeholder. `scripts/gen-icons.mjs` generates `resources/icon.icns`, `resources/icon.ico`, and `resources/icon.png` from `public/branding/swarmclaw-org-avatar.png`; the main process sets the Dock icon at launch and passes it to every `BrowserWindow`.
|
|
444
|
-
- **Embedded server log file + improved failure dialog**: the Electron wrapper now tees the child Next.js server's stdout/stderr into `<userData>/logs/server.log` (`~/Library/Application Support/@swarmclawai/swarmclaw/logs/server.log` on macOS, 1 MB rotation). If startup fails or the server exits, the error dialog shows the tail of the log inline and exposes an **Open Logs Folder** button that jumps Finder straight to the file. This is what made root-cause debugging possible in the first place — if you hit any kind of regression here, grab that log and open an issue.
|
|
445
|
-
- **Embedded server timeout raised from 60s to 5 minutes**: a safety net. On a healthy install the server is up in about a second; 300 seconds is there for pathological cold boots (very large data dirs, contested Apple Silicon Gatekeeper verification on unsigned binaries, etc.) and should never be hit in normal use.
|
|
446
|
-
|
|
447
|
-
### v1.5.50 Highlights
|
|
448
|
-
|
|
449
|
-
- **Fix: opencode-web remote instances no longer fail with `EACCES`**: SwarmClaw used to send the local workspace path (e.g. `/root/.swarmclaw/workspace`) as a `directory=` query parameter on every opencode-web request. Remote opencode-web instances tried to `lstat` that path and rejected the call. The provider now auto-detects local vs. remote from the endpoint hostname (`localhost`, `127.0.0.1`, `::1`, `0.0.0.0`) and only sends `directory=` when the endpoint is local. Thanks to [@SteamedFish](https://github.com/SteamedFish) for the detailed root-cause writeup in [#45](https://github.com/swarmclawai/swarmclaw/issues/45).
|
|
450
|
-
|
|
451
|
-
### v1.5.49 Highlights
|
|
452
|
-
|
|
453
|
-
- **Autonomous Missions**: a new first-class concept for long-running, goal-driven agent work. Hand your agent team a goal on Friday, come back Monday to see what they shipped. Each mission carries a title, a natural-language objective, bulleted success criteria, hard budgets (USD, tokens, turns, wallclock), periodic markdown reports, and a full milestone timeline. Missions drive any session through the existing heartbeat pipeline, so delegation to Claude Code, Codex, OpenCode, Cursor, Droid, Goose, Qwen, or native SwarmClaw agents all work without changes.
|
|
454
|
-
- **Budget enforcement in the run pipeline**: `enqueueSessionRun` now consults the mission's budget before every autonomous turn. When any cap is hit the mission transitions to `budget_exhausted`, the queue drains, and a final report fires. Warn thresholds (default 50% / 80% / 95% of each cap) emit `budget_warn` milestones exactly once each.
|
|
455
|
-
- **Scheduler tick from heartbeat**: `runMissionScheduler()` fires every heartbeat tick, independent of the active-hours window, so wallclock budgets and periodic reports still fire overnight. Report cadence is configurable per mission; reports land as in-app notifications today and ship as Slack/Discord/audio in a follow-up.
|
|
456
|
-
- **`/missions` dashboard**: new page with a live mission list, status pills, four-axis budget gauges, a scrollable milestone timeline, a reports drawer, and start / pause / cancel / mark-complete / generate-report-now controls.
|
|
457
|
-
- **CLI commands**: `swarmclaw missions list|get|create|update|delete|control|reports|report-now|events`. Create a mission, start it, and watch the timeline from the terminal or CI.
|
|
458
|
-
- **New storage collections**: `agent_missions`, `mission_reports`, and `agent_mission_events`. The legacy deprecated `missions` table is left untouched so nothing in existing installs is disturbed.
|
|
459
|
-
|
|
460
|
-
### v1.5.48 Highlights
|
|
461
|
-
|
|
462
|
-
- **SwarmDock MCP preset now points at the hosted endpoint**: *MCP Servers → Quick Setup → SwarmDock* is pre-filled with `streamable-http` transport pointed at `https://swarmdock-api.onrender.com/mcp` and a ready-to-edit `Authorization: Bearer <key>` header template. Users no longer need to run `npx swarmdock-mcp` locally — the SwarmDock team hosts the MCP server in-process on the existing API service. First-time setup (browser keygen + agent registration) lives at [swarmdock.ai/mcp/connect](https://www.swarmdock.ai/mcp/connect).
|
|
463
|
-
- **McpPreset gains `url` and `headersTemplate`**: `applyPreset` now prefills the URL input and the Headers textarea in addition to command/args/env, so remote presets can ship complete configs.
|
|
464
|
-
- **Skills doc refresh**: the `swarmclaw` skill's MCP Servers section points to the hosted flow instead of the prior stdio instructions.
|
|
465
|
-
|
|
466
|
-
### v1.5.47 Highlights
|
|
467
|
-
|
|
468
|
-
- **MCP injection for GitHub Copilot CLI and OpenAI Codex CLI agents**: agents using the `copilot-cli` or `codex-cli` providers now run with their assigned MCP servers attached at runtime. Copilot CLI receives the servers via `--additional-mcp-config @<tempfile>`; Codex CLI gets per-session `[mcp_servers.*]` TOML sections appended to a scoped `config.toml`. Stdio transports (command, args, env, cwd) and SSE / streamable-http transports (url, headers) are both supported. Skills assigned to the agent continue to be injected via the system prompt.
|
|
469
|
-
- **Skills and MCP panel visible for copilot-cli and codex-cli in the agent editor**: the Advanced Settings section now opens for these two providers so you can attach skills and MCP servers from the UI. Routing, memory, and voice panels stay hidden since these providers are worker-only.
|
|
470
|
-
- **Codex CLI approval policy change**: Codex CLI sessions now launch with `--dangerously-bypass-approvals-and-sandbox` instead of `--full-auto`. The old flag silently cancels MCP tool calls via Codex's approval gate, which is why MCP tool results were not landing. SwarmClaw itself runs in its own sandbox, so Codex's additional sandbox was not load-bearing, but be aware of the change if you were relying on it for a specific agent.
|
|
471
|
-
- **Under the hood**: `~/.codex-sessions/<session.id>/` replaces `/tmp/swarmclaw-codex-*` as the per-session Codex config directory because Codex refuses to create helper binaries under `/tmp`. The Playwright MCP proxy now passes an explicit `cwd: process.cwd()` when spawning, so it no longer crashes with `uv_cwd ENOENT` when the server is restarted after a directory move.
|
|
472
|
-
- **Exa as a new web search provider**: Settings > Web Search gains an Exa option alongside Tavily, Brave, SearXNG, DuckDuckGo, Google, and Bing. Exa uses neural search with AI-generated summaries and falls back to highlights, then raw text when summaries are unavailable. Configure the key via the UI, the `EXA_API_KEY` environment variable, or the secrets store. Requests carry an `x-exa-integration: swarmclaw` tracking header so usage attributed to SwarmClaw is visible to Exa.
|
|
473
|
-
|
|
474
|
-
Thanks to [@borislavnnikolov](https://github.com/borislavnnikolov) and [@tgonzalezc5](https://github.com/tgonzalezc5) for the contributions.
|
|
475
|
-
|
|
476
|
-
### v1.5.46 Highlights
|
|
477
|
-
|
|
478
|
-
- **Custom base URL for built-in OpenAI and Anthropic providers**: the Endpoint field in provider settings now works for the built-in OpenAI and Anthropic providers (marked as `optionalEndpoint`). Point them at a proxy, gateway, or self-hosted endpoint and the URL persists, auto-resolves on connection test, and flows through both the live chat path and the LangGraph agent path (`ChatAnthropic` now receives `anthropicApiUrl`). Existing installs with no custom URL keep using the defaults.
|
|
479
|
-
- **Test-model selector in provider settings**: when you hit "Test Connection", a new dropdown lets you pick a specific model (for example `gpt-4.1-mini` or `claude-haiku-4-5`) or leave it on Auto-detect. Useful for verifying a specific model is reachable on a given endpoint.
|
|
480
|
-
- **Auto-resolution of credentials and endpoints in the connection test**: the test route now looks up the saved credential and base URL for the provider when they are not explicitly supplied, so the provider sheet's "Test" button works without needing to replay config.
|
|
481
|
-
- **Anthropic streaming refactor**: the streaming handler moved from Node's `https.request()` to `fetch()`. Same behavior, cleaner cancellation, and it now respects `session.apiEndpoint` as a full base URL instead of a hostname.
|
|
482
|
-
- **Connection test body**: Ollama and OpenAI-compatible test requests now send `max_completion_tokens` instead of the legacy `max_tokens`, matching current OpenAI conventions and working correctly with reasoning models that reject `max_tokens`.
|
|
483
|
-
|
|
484
|
-
Thanks to [@Llugaes](https://github.com/Llugaes) for the contribution.
|
|
485
|
-
|
|
486
|
-
### v1.5.45 Highlights
|
|
487
|
-
|
|
488
|
-
- **SwarmVault MCP preset**: a new "SwarmVault" Quick Setup chip in the MCP server sheet pre-fills `npx -y @swarmvaultai/cli mcp` over `stdio` and prompts for the vault directory. One click registers a SwarmVault knowledge vault as an MCP server; agents pick it up via the existing per-agent MCP server selector. SwarmVault docs: https://swarmvault.ai
|
|
489
|
-
- **`cwd` on stdio MCP servers**: `McpServerConfig` now has an optional `cwd` field. The MCP client passes it through to `StdioClientTransport` so servers that discover config from the working directory (SwarmVault, anything that reads from `cwd`-relative files) work correctly. Existing MCP servers are untouched (the field is optional and defaults to the SwarmClaw process cwd, which was the prior behaviour).
|
|
490
|
-
- **Bundled `swarmvault` skill**: ships at `skills/swarmvault/SKILL.md` and is auto-discovered alongside the other bundled skills. Captures the schema-first / graph-query-first conventions (read `swarmvault.schema.md` before compile or query work, treat `raw/` as immutable, prefer `graph query|path|explain` over grep, preserve `page_id` / `source_ids` / `node_ids` / `freshness` / `source_hashes` frontmatter, save high-value answers to `wiki/outputs/`). Pin it on any agent that talks to a SwarmVault vault. Optional and decoupled from the MCP integration.
|
|
491
|
-
|
|
492
|
-
### v1.5.44 Highlights
|
|
493
|
-
|
|
494
|
-
- **Model lists refreshed across every provider**: dropdowns now lead with the April-2026 flagship models instead of mid-2025 names. OpenAI goes to GPT-5.4 / 5.4-mini / 5.4-nano / 5.3 / o3-mini. Google and Gemini CLI lead with Gemini 3.1 Pro, Gemini 3 Flash, and 3.1 Flash-Lite, keeping 2.5 as a legacy fallback. xAI jumps from Grok 3 to Grok 4 plus the Grok 4 / 4.1 Fast reasoning and non-reasoning variants. Groq drops the deprecated `deepseek-r1-distill-llama-70b` and leads with Llama 4 Maverick, Llama 4 Scout, Kimi K2, and gpt-oss 120b/20b. Mistral moves to Magistral 1.2, Devstral 2, Codestral, and Mistral Small 4. Fireworks / Nebius / DeepInfra now lead with DeepSeek V3.2, Kimi K2.5, and Qwen 3 235B instead of the older R1-0528 checkpoint. Anthropic and Claude CLI reorder Opus 4.6 / Sonnet 4.6 / Haiku 4.5 newest-first. OpenCode Web refreshes its `providerID/modelID` seed list.
|
|
495
|
-
- **OpenRouter default set expanded**: was one model (`openai/gpt-4.1-mini`). Now ten flagship routes including `openrouter/auto`, Claude 4.6 Opus / Sonnet / Haiku, GPT-5.4, Gemini 3.1 Pro / 3 Flash, Grok 4, DeepSeek V3.2, and Llama 4 Maverick. Much better first-run experience for the "provider that routes to every other provider".
|
|
496
|
-
- **`DEFAULT_AGENTS` models refreshed**: 11 starter-agent models updated to match the new flagship lineups (OpenAI → GPT-5.4, xAI → Grok 4, Google / Gemini CLI → Gemini 3.1 Pro, Groq → Llama 4 Maverick, Fireworks / Nebius / DeepInfra → DeepSeek V3.2, OpenCode Web / Copilot CLI → Claude Sonnet 4.6, OpenRouter → Claude Sonnet 4.6). Starter agents created from the setup wizard now default to the right model out of the box.
|
|
497
|
-
- **Starter-agent tool bundles now include `droid_cli` and `copilot_cli`**: these delegation backends were added in v1.5.37 and v1.5.3 respectively but never made it into `STARTER_AGENT_TOOLS` / `BUILDER_AGENT_TOOLS`. Every starter kit (Sidekick, Researcher, Builder, Reviewer, Operator, OpenClaw fleet) now picks them up on new workspace creation.
|
|
498
|
-
- **DeepSeek note**: `deepseek-chat` and `deepseek-reasoner` remain the recommended model names — they are stable aliases that auto-track the current `V3.2` weights. No action required.
|
|
499
|
-
- **Registry sanity test**: added `provider-models.test.ts` which asserts every provider declares a non-empty deduplicated models array, matching metadata keys, and a working `handler.streamChat`. Guards against future copy-paste regressions in the registry.
|
|
500
|
-
|
|
501
|
-
### v1.5.43 Highlights
|
|
502
|
-
|
|
503
|
-
- **`/api/version` no longer 500s in Docker**: the route used to shell out to `git` at runtime, which fails in the production image because `.git/` is not copied. The route now returns 200 with `{ source: 'package', version }` from `package.json` when git metadata is unavailable, and `{ source: 'git', version, commit, ... }` when it is. `/api/version/update` short-circuits on Docker-style installs with a clear `no_git_metadata` reason instead of an opaque 500. ([#41](https://github.com/swarmclawai/swarmclaw/issues/41) Bug 1, reported by [@SteamedFish](https://github.com/SteamedFish).)
|
|
504
|
-
- **Daemon reclaims stale `daemon-primary` leases on container restart**: when the previous container died holding the SQLite-backed lease, the new container previously waited up to the full 120 s TTL before the daemon could start. The successor now parses the recorded owner pid, probes it with `process.kill(pid, 0)`, and reclaims the lease immediately when the prior owner is provably dead on this host. When the owner is genuinely alive (or when the recorded host is ambiguous, such as multi-pod Kubernetes), behaviour is unchanged but a single deferred retry is scheduled just past the TTL so the daemon comes up automatically rather than waiting for the next API call. ([#41](https://github.com/swarmclawai/swarmclaw/issues/41) Bug 2.)
|
|
505
|
-
- **Subprocess daemon fallback fails soft in Docker**: when `resolveDaemonRuntimeEntry()` cannot find `src/lib/server/daemon/daemon-runtime.ts` (the file is intentionally not in the standalone build), `ensureDaemonProcessRunning()` now logs a one-shot warning and returns `false` instead of throwing into the API handler. The in-process daemon path (with the Bug 2 fix) is the production path in Docker. ([#41](https://github.com/swarmclawai/swarmclaw/issues/41) Bug 3.)
|
|
506
|
-
- **`CONTRIBUTING.md`**: dropped the broken reference to `AGENTS.md`. That file is `.gitignore`'d and not visible to external contributors. The single canonical project-conventions document is `CLAUDE.md`.
|
|
507
|
-
|
|
508
|
-
### v1.5.42 Highlights
|
|
509
|
-
|
|
510
|
-
- **New `opencode-web` provider — connect to remote OpenCode HTTP servers** ([#40](https://github.com/swarmclawai/swarmclaw/issues/40), requested by [@SteamedFish](https://github.com/SteamedFish)): point an agent at any host running `opencode serve` or `opencode web` (default port `4096`). Supports HTTPS endpoints, HTTP Basic Auth (encode credentials as `username:password` in the API key field; bare passwords default the username to `opencode`), automatic OpenCode session reuse across chat turns, and per-session workspace isolation via `?directory=...`. Models are entered as `providerID/modelID` (e.g. `anthropic/claude-sonnet-4-5`). The existing `opencode-cli` provider is unchanged.
|
|
511
|
-
- **New `CONTRIBUTING.md`**: short, scannable guide covering bug reports, feature requests, PR expectations, commit conventions, and where to look in the codebase. Models the gold-standard examples after issues #39 and #40.
|
|
512
|
-
- **`GET /api/memory/:id` now returns a single entry by default**: previously it eagerly traversed linked memories and returned an array, which broke naive callers that expected a single object per REST convention. Linked traversal is now opt-in via `?depth=N` or `?envelope=true`.
|
|
513
|
-
|
|
514
|
-
### v1.5.41 Highlights
|
|
515
|
-
|
|
516
|
-
- **Moonshot / Kimi compatibility — duplicate `files` tool name fixed**: any agent with the default `files` extension was sending two tools both literally named `files` to the LLM. Most providers tolerated the duplicate; Moonshot's strict tool-schema validation rejected it with `MoonshotException - function name files is duplicated` ([#39](https://github.com/swarmclawai/swarmclaw/issues/39), reported by [@SteamedFish](https://github.com/SteamedFish)). Three fixes: the v2 file builder is now correctly gated on `files_v2` (not `files`), it registers under the matching capability key, and the session-tools assembler now shares a single dedup Set across native, CRUD, and extension phases so any future name collision is rejected with a clear warning instead of a silent double-register.
|
|
517
|
-
|
|
518
|
-
### v1.5.40 Highlights
|
|
519
|
-
|
|
520
|
-
- **Current-thread recall routing**: the message classifier now emits four explicit flags (`isCurrentThreadRecall`, `isGreeting`, `isAcknowledgement`, `isMemoryWriteIntent`) so the chat router stops treating in-thread pronouns ("your last reply", "both answers", "what I just said") as durable-memory queries. Previously small OSS models (`devstral-small-2:24b` and similar) would run `memory_search` for these, come back empty, and truthfully report "no memories found" even when the answer was three messages up.
|
|
521
|
-
- **`memory_search` short-circuits thread-recall queries**: when the search query itself contains phrases like "just", "last reply", "my last", "both answers", the tool now returns a redirect pointing the model back to the visible chat history instead of executing a pointless vector search. Explicit cross-session phrasing ("yesterday", "last week", "in a previous conversation") still runs the normal search path.
|
|
522
|
-
- **Explicit Routing Matrix in the system prompt**: spells out the boundary between "read the thread above" and "call a memory tool" in plain language, so routing doesn't depend on the model extrapolating a terse rule. Memory-tool lines are now tagged `(not this thread)` so the distinction is unmissable.
|
|
523
|
-
- **Tool-summary retry threshold tightened**: the "trivial response" threshold used to decide whether to force a redundant `tool_summary` continuation dropped from 150 → 80 characters. A 119-char response like "I wrote X, stored Y, and confirmed both." is substantive; the old threshold forced the model to re-stream the same answer twice.
|
|
524
|
-
- **Classifier timeout raised to 10 s**: 2 s was too tight for Ollama Cloud with a fully-configured agent (observed 4–6 s calls). Result caching means the latency tax only applies to first-seen messages.
|
|
525
|
-
- **Reflection memories dedup across runs**: the supervisor reflection writer now compares candidate notes against recent (last 7 days) reflection memories for the same agent and skips ones that have already been stored, stopping the ~7-per-turn rediscovery churn on top of the within-run dedup shipped in v1.5.38.
|
|
526
|
-
|
|
527
449
|
Older releases: https://swarmclaw.ai/docs/release-notes
|
|
528
450
|
|
|
529
451
|
- GitHub releases: https://github.com/swarmclawai/swarmclaw/releases
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.58",
|
|
4
4
|
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
|
|
5
5
|
"main": "electron-dist/main.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
3
|
+
import { setChatroomRefusalPolicy, handleAgentRefusal } from '@/lib/server/chatrooms/chatroom-refusal'
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic'
|
|
6
|
+
|
|
7
|
+
const VALID_POLICIES = new Set(['reroute', 'escalate', 'human'])
|
|
8
|
+
|
|
9
|
+
export async function POST(req: Request) {
|
|
10
|
+
const { data: body, error } = await safeParseBody(req)
|
|
11
|
+
if (error) return error
|
|
12
|
+
const chatroomId = typeof body?.chatroomId === 'string' ? body.chatroomId : null
|
|
13
|
+
const policy = typeof body?.policy === 'string' ? body.policy : null
|
|
14
|
+
if (!chatroomId) return NextResponse.json({ error: 'chatroomId required' }, { status: 400 })
|
|
15
|
+
if (!policy || !VALID_POLICIES.has(policy)) {
|
|
16
|
+
return NextResponse.json({ error: 'policy must be reroute|escalate|human' }, { status: 400 })
|
|
17
|
+
}
|
|
18
|
+
const escalationTargetAgentId = typeof body?.escalationTargetAgentId === 'string'
|
|
19
|
+
? body.escalationTargetAgentId
|
|
20
|
+
: null
|
|
21
|
+
const room = setChatroomRefusalPolicy(
|
|
22
|
+
chatroomId,
|
|
23
|
+
policy as 'reroute' | 'escalate' | 'human',
|
|
24
|
+
escalationTargetAgentId,
|
|
25
|
+
)
|
|
26
|
+
if (!room) return NextResponse.json({ error: 'chatroom not found' }, { status: 404 })
|
|
27
|
+
return NextResponse.json({ chatroom: room })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function PUT(req: Request) {
|
|
31
|
+
// Trigger a refusal handling decision (used by agent runtime + tests).
|
|
32
|
+
const { data: body, error } = await safeParseBody(req)
|
|
33
|
+
if (error) return error
|
|
34
|
+
const chatroomId = typeof body?.chatroomId === 'string' ? body.chatroomId : null
|
|
35
|
+
const refusingAgentId = typeof body?.refusingAgentId === 'string' ? body.refusingAgentId : null
|
|
36
|
+
const taskOrTopic = typeof body?.taskOrTopic === 'string' ? body.taskOrTopic : ''
|
|
37
|
+
const reason = typeof body?.reason === 'string' ? body.reason : 'unspecified'
|
|
38
|
+
if (!chatroomId || !refusingAgentId) {
|
|
39
|
+
return NextResponse.json({ error: 'chatroomId and refusingAgentId required' }, { status: 400 })
|
|
40
|
+
}
|
|
41
|
+
const decision = handleAgentRefusal({ chatroomId, refusingAgentId, taskOrTopic, reason })
|
|
42
|
+
return NextResponse.json({ decision })
|
|
43
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
3
|
+
import { getVersion } from '@/lib/server/config-versions/config-version-repository'
|
|
4
|
+
import { updateAgent } from '@/lib/server/agents/agent-service'
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic'
|
|
7
|
+
|
|
8
|
+
export async function POST(req: Request) {
|
|
9
|
+
const { data: body, error } = await safeParseBody(req)
|
|
10
|
+
if (error) return error
|
|
11
|
+
const versionId = typeof body?.versionId === 'string' ? body.versionId : null
|
|
12
|
+
if (!versionId) return NextResponse.json({ error: 'versionId required' }, { status: 400 })
|
|
13
|
+
|
|
14
|
+
const version = getVersion(versionId)
|
|
15
|
+
if (!version) return NextResponse.json({ error: 'version not found' }, { status: 404 })
|
|
16
|
+
|
|
17
|
+
if (version.entityKind === 'agent') {
|
|
18
|
+
const restored = updateAgent(version.entityId, version.snapshot)
|
|
19
|
+
if (!restored) return NextResponse.json({ error: 'agent not found' }, { status: 404 })
|
|
20
|
+
return NextResponse.json({ ok: true, restored: { kind: 'agent', id: version.entityId } })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return NextResponse.json({
|
|
24
|
+
error: `Restore not yet implemented for kind=${version.entityKind}`,
|
|
25
|
+
}, { status: 501 })
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { listVersionsForEntity } from '@/lib/server/config-versions/config-version-repository'
|
|
3
|
+
import type { VersionedEntityKind } from '@/types/config-version'
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic'
|
|
6
|
+
|
|
7
|
+
const VALID_KINDS = new Set<VersionedEntityKind>([
|
|
8
|
+
'agent', 'extension', 'connector', 'mcp_server', 'chatroom', 'project',
|
|
9
|
+
])
|
|
10
|
+
|
|
11
|
+
export async function GET(req: Request) {
|
|
12
|
+
const { searchParams } = new URL(req.url)
|
|
13
|
+
const kind = searchParams.get('entityKind') as VersionedEntityKind | null
|
|
14
|
+
const id = searchParams.get('entityId')
|
|
15
|
+
if (!kind || !VALID_KINDS.has(kind)) {
|
|
16
|
+
return NextResponse.json({ error: 'entityKind required (agent|extension|connector|mcp_server|chatroom|project)' }, { status: 400 })
|
|
17
|
+
}
|
|
18
|
+
if (!id) return NextResponse.json({ error: 'entityId required' }, { status: 400 })
|
|
19
|
+
const versions = listVersionsForEntity(kind, id)
|
|
20
|
+
return NextResponse.json({ versions })
|
|
21
|
+
}
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import { EVAL_SCENARIOS } from '@/lib/server/eval/scenarios'
|
|
2
|
+
import { EVAL_SCENARIOS, getSuiteScenarios } from '@/lib/server/eval/scenarios'
|
|
3
3
|
|
|
4
4
|
export async function GET(req: Request) {
|
|
5
5
|
const { searchParams } = new URL(req.url)
|
|
6
6
|
const category = searchParams.get('category')
|
|
7
|
+
const suite = searchParams.get('suite')
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
let scenarios = EVAL_SCENARIOS
|
|
10
|
+
if (suite) scenarios = getSuiteScenarios(suite)
|
|
11
|
+
if (category) scenarios = scenarios.filter((s) => s.category === category)
|
|
11
12
|
|
|
12
13
|
return NextResponse.json(
|
|
13
14
|
scenarios.map((s) => ({
|
|
14
15
|
id: s.id,
|
|
15
16
|
name: s.name,
|
|
16
17
|
category: s.category,
|
|
18
|
+
suite: s.suite ?? 'core',
|
|
17
19
|
description: s.description,
|
|
18
20
|
tools: s.tools,
|
|
19
21
|
timeoutMs: s.timeoutMs,
|
|
@@ -6,6 +6,7 @@ import { errorMessage } from '@/lib/shared-utils'
|
|
|
6
6
|
const SuiteSchema = z.object({
|
|
7
7
|
agentId: z.string().min(1),
|
|
8
8
|
categories: z.array(z.string()).optional(),
|
|
9
|
+
suite: z.string().min(1).optional(),
|
|
9
10
|
})
|
|
10
11
|
|
|
11
12
|
export async function POST(req: Request) {
|
|
@@ -19,7 +20,10 @@ export async function POST(req: Request) {
|
|
|
19
20
|
)
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
const result = await runEvalSuite(parsed.data.agentId,
|
|
23
|
+
const result = await runEvalSuite(parsed.data.agentId, {
|
|
24
|
+
categories: parsed.data.categories,
|
|
25
|
+
suite: parsed.data.suite,
|
|
26
|
+
})
|
|
23
27
|
return NextResponse.json(result)
|
|
24
28
|
} catch (err: unknown) {
|
|
25
29
|
return NextResponse.json(
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { EVAL_SCENARIOS, getSuiteScenarios, listSuites } from '@/lib/server/eval/scenarios'
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
const suites = listSuites()
|
|
6
|
+
const summary = suites.map((name) => {
|
|
7
|
+
const scenarios = name === 'core' ? EVAL_SCENARIOS.filter(s => !s.suite || s.suite === 'core') : getSuiteScenarios(name)
|
|
8
|
+
return {
|
|
9
|
+
name,
|
|
10
|
+
count: scenarios.length,
|
|
11
|
+
maxScore: scenarios.reduce(
|
|
12
|
+
(sum, s) => sum + s.scoringCriteria.reduce((a, c) => a + c.weight, 0),
|
|
13
|
+
0,
|
|
14
|
+
),
|
|
15
|
+
categories: Array.from(new Set(scenarios.map(s => s.category))),
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
return NextResponse.json(summary)
|
|
19
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { genId } from '@/lib/id'
|
|
3
|
+
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
4
|
+
import {
|
|
5
|
+
deleteWorkflowState,
|
|
6
|
+
listWorkflowStates,
|
|
7
|
+
resetWorkflowStatesToDefaults,
|
|
8
|
+
upsertWorkflowState,
|
|
9
|
+
} from '@/lib/server/tasks/workflow-state-repository'
|
|
10
|
+
import type { WorkflowState, WorkflowStateCategory } from '@/types/workflow-state'
|
|
11
|
+
|
|
12
|
+
export const dynamic = 'force-dynamic'
|
|
13
|
+
|
|
14
|
+
const VALID_CATEGORIES = new Set<WorkflowStateCategory>([
|
|
15
|
+
'triage', 'backlog', 'unstarted', 'started', 'completed', 'cancelled',
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
function asString(value: unknown): string | null {
|
|
19
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function GET(req: Request) {
|
|
23
|
+
const { searchParams } = new URL(req.url)
|
|
24
|
+
const projectId = searchParams.get('projectId')
|
|
25
|
+
const states = listWorkflowStates(
|
|
26
|
+
projectId ? { projectId } : undefined,
|
|
27
|
+
)
|
|
28
|
+
return NextResponse.json({ states })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function POST(req: Request) {
|
|
32
|
+
const { data: body, error } = await safeParseBody(req)
|
|
33
|
+
if (error) return error
|
|
34
|
+
const label = asString(body?.label)
|
|
35
|
+
const categoryRaw = asString(body?.category) as WorkflowStateCategory | null
|
|
36
|
+
if (!label) return NextResponse.json({ error: 'label is required' }, { status: 400 })
|
|
37
|
+
if (!categoryRaw || !VALID_CATEGORIES.has(categoryRaw)) {
|
|
38
|
+
return NextResponse.json({ error: 'category must be one of triage|backlog|unstarted|started|completed|cancelled' }, { status: 400 })
|
|
39
|
+
}
|
|
40
|
+
const id = asString(body?.id) || genId()
|
|
41
|
+
const now = Date.now()
|
|
42
|
+
const state: WorkflowState = {
|
|
43
|
+
id,
|
|
44
|
+
label,
|
|
45
|
+
category: categoryRaw,
|
|
46
|
+
projectId: asString(body?.projectId),
|
|
47
|
+
color: asString(body?.color),
|
|
48
|
+
position: typeof body?.position === 'number' ? body.position : undefined,
|
|
49
|
+
autoArchiveAfterDays: typeof body?.autoArchiveAfterDays === 'number' ? body.autoArchiveAfterDays : null,
|
|
50
|
+
createdAt: now,
|
|
51
|
+
updatedAt: now,
|
|
52
|
+
}
|
|
53
|
+
const saved = upsertWorkflowState(state)
|
|
54
|
+
return NextResponse.json({ state: saved })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function DELETE(req: Request) {
|
|
58
|
+
const { searchParams } = new URL(req.url)
|
|
59
|
+
const id = searchParams.get('id')
|
|
60
|
+
if (id) {
|
|
61
|
+
const removed = deleteWorkflowState(id)
|
|
62
|
+
return NextResponse.json({ ok: removed })
|
|
63
|
+
}
|
|
64
|
+
if (searchParams.get('reset') === 'true') {
|
|
65
|
+
resetWorkflowStatesToDefaults()
|
|
66
|
+
return NextResponse.json({ ok: true, reset: true })
|
|
67
|
+
}
|
|
68
|
+
return NextResponse.json({ error: 'id or reset=true required' }, { status: 400 })
|
|
69
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { listObservedBillingCodes, rollupCostByBillingCode } from '@/lib/server/usage/cost-attribution'
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
const RANGE_MS: Record<string, number> = {
|
|
7
|
+
'24h': 24 * 60 * 60 * 1000,
|
|
8
|
+
'7d': 7 * 24 * 60 * 60 * 1000,
|
|
9
|
+
'30d': 30 * 24 * 60 * 60 * 1000,
|
|
10
|
+
'all': Number.POSITIVE_INFINITY,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function GET(req: Request) {
|
|
14
|
+
const { searchParams } = new URL(req.url)
|
|
15
|
+
const codesParam = searchParams.get('codes')
|
|
16
|
+
const codes = codesParam
|
|
17
|
+
? codesParam.split(',').map((s) => s.trim()).filter(Boolean)
|
|
18
|
+
: undefined
|
|
19
|
+
const rangeParam = searchParams.get('range') ?? 'all'
|
|
20
|
+
const rangeMs = RANGE_MS[rangeParam] ?? Number.POSITIVE_INFINITY
|
|
21
|
+
const sinceMs = Number.isFinite(rangeMs) ? Date.now() - rangeMs : 0
|
|
22
|
+
|
|
23
|
+
const rollups = rollupCostByBillingCode({ codes, sinceMs })
|
|
24
|
+
const observed = listObservedBillingCodes()
|
|
25
|
+
|
|
26
|
+
return NextResponse.json({
|
|
27
|
+
range: rangeParam,
|
|
28
|
+
codes: codes ?? null,
|
|
29
|
+
rollups,
|
|
30
|
+
observedCodes: observed,
|
|
31
|
+
})
|
|
32
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadUsage, loadSessions } from '@/lib/server/storage'
|
|
3
|
+
import type { UsageRecord } from '@/types'
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic'
|
|
6
|
+
|
|
7
|
+
type SessionSnapshot = {
|
|
8
|
+
id?: string
|
|
9
|
+
agentId?: string
|
|
10
|
+
createdAt?: number
|
|
11
|
+
lastActiveAt?: number
|
|
12
|
+
messages?: unknown[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface LiveUsage {
|
|
16
|
+
sessionId: string
|
|
17
|
+
records: number
|
|
18
|
+
totalTokens: number
|
|
19
|
+
inputTokens: number
|
|
20
|
+
outputTokens: number
|
|
21
|
+
estimatedCost: number
|
|
22
|
+
firstAt: number | null
|
|
23
|
+
lastAt: number | null
|
|
24
|
+
wallclockMs: number
|
|
25
|
+
turns: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function summarize(sessionId: string, records: UsageRecord[], session: SessionSnapshot | undefined): LiveUsage {
|
|
29
|
+
let totalTokens = 0
|
|
30
|
+
let inputTokens = 0
|
|
31
|
+
let outputTokens = 0
|
|
32
|
+
let estimatedCost = 0
|
|
33
|
+
let firstAt: number | null = null
|
|
34
|
+
let lastAt: number | null = null
|
|
35
|
+
|
|
36
|
+
for (const r of records) {
|
|
37
|
+
totalTokens += r.totalTokens || 0
|
|
38
|
+
inputTokens += r.inputTokens || 0
|
|
39
|
+
outputTokens += r.outputTokens || 0
|
|
40
|
+
estimatedCost += r.estimatedCost || 0
|
|
41
|
+
const ts = r.timestamp || 0
|
|
42
|
+
if (ts > 0) {
|
|
43
|
+
if (firstAt === null || ts < firstAt) firstAt = ts
|
|
44
|
+
if (lastAt === null || ts > lastAt) lastAt = ts
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const turns = Array.isArray(session?.messages) ? session!.messages!.length : records.length
|
|
49
|
+
const wallStart = session?.createdAt ?? firstAt ?? 0
|
|
50
|
+
const wallEnd = session?.lastActiveAt ?? lastAt ?? Date.now()
|
|
51
|
+
const wallclockMs = wallStart > 0 ? Math.max(0, wallEnd - wallStart) : 0
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
sessionId,
|
|
55
|
+
records: records.length,
|
|
56
|
+
totalTokens,
|
|
57
|
+
inputTokens,
|
|
58
|
+
outputTokens,
|
|
59
|
+
estimatedCost: Math.round(estimatedCost * 10000) / 10000,
|
|
60
|
+
firstAt,
|
|
61
|
+
lastAt,
|
|
62
|
+
wallclockMs,
|
|
63
|
+
turns,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function GET(req: Request) {
|
|
68
|
+
const { searchParams } = new URL(req.url)
|
|
69
|
+
const sessionId = searchParams.get('sessionId')?.trim()
|
|
70
|
+
|
|
71
|
+
const usage = loadUsage() as Record<string, UsageRecord[]>
|
|
72
|
+
const sessions = loadSessions() as Record<string, SessionSnapshot>
|
|
73
|
+
|
|
74
|
+
if (sessionId) {
|
|
75
|
+
const records = usage[sessionId] ?? []
|
|
76
|
+
const session = sessions[sessionId]
|
|
77
|
+
return NextResponse.json(summarize(sessionId, records, session))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Without sessionId, return the 10 most recently active sessions
|
|
81
|
+
const ids = Object.keys(usage)
|
|
82
|
+
const recent = ids
|
|
83
|
+
.map((id) => {
|
|
84
|
+
const records = usage[id] ?? []
|
|
85
|
+
const last = records.reduce((m, r) => Math.max(m, r.timestamp || 0), 0)
|
|
86
|
+
return { id, last }
|
|
87
|
+
})
|
|
88
|
+
.sort((a, b) => b.last - a.last)
|
|
89
|
+
.slice(0, 10)
|
|
90
|
+
|
|
91
|
+
return NextResponse.json(
|
|
92
|
+
recent.map(({ id }) => summarize(id, usage[id] ?? [], sessions[id])),
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
3
|
+
import { getActiveWorkspace, setActiveWorkspace } from '@/lib/server/workspaces/workspace-registry'
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic'
|
|
6
|
+
|
|
7
|
+
export async function GET() {
|
|
8
|
+
return NextResponse.json({ workspace: getActiveWorkspace() })
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function POST(req: Request) {
|
|
12
|
+
const { data: body, error } = await safeParseBody(req)
|
|
13
|
+
if (error) return error
|
|
14
|
+
const id = typeof body?.id === 'string' ? body.id : null
|
|
15
|
+
if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 })
|
|
16
|
+
const workspace = setActiveWorkspace(id)
|
|
17
|
+
if (!workspace) return NextResponse.json({ error: 'workspace not found' }, { status: 404 })
|
|
18
|
+
return NextResponse.json({ workspace })
|
|
19
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
3
|
+
import {
|
|
4
|
+
createWorkspace,
|
|
5
|
+
deleteWorkspace,
|
|
6
|
+
listWorkspaces,
|
|
7
|
+
updateWorkspace,
|
|
8
|
+
} from '@/lib/server/workspaces/workspace-registry'
|
|
9
|
+
|
|
10
|
+
export const dynamic = 'force-dynamic'
|
|
11
|
+
|
|
12
|
+
export async function GET() {
|
|
13
|
+
return NextResponse.json({ workspaces: listWorkspaces() })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function POST(req: Request) {
|
|
17
|
+
const { data: body, error } = await safeParseBody(req)
|
|
18
|
+
if (error) return error
|
|
19
|
+
const name = typeof body?.name === 'string' && body.name.trim() ? body.name.trim() : null
|
|
20
|
+
if (!name) return NextResponse.json({ error: 'name required' }, { status: 400 })
|
|
21
|
+
const workspace = createWorkspace({
|
|
22
|
+
name,
|
|
23
|
+
description: typeof body?.description === 'string' ? body.description : undefined,
|
|
24
|
+
color: typeof body?.color === 'string' ? body.color : undefined,
|
|
25
|
+
})
|
|
26
|
+
return NextResponse.json({ workspace })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function PATCH(req: Request) {
|
|
30
|
+
const { data: body, error } = await safeParseBody(req)
|
|
31
|
+
if (error) return error
|
|
32
|
+
const id = typeof body?.id === 'string' ? body.id : null
|
|
33
|
+
if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 })
|
|
34
|
+
const updated = updateWorkspace(id, body as Record<string, unknown>)
|
|
35
|
+
if (!updated) return NextResponse.json({ error: 'workspace not found' }, { status: 404 })
|
|
36
|
+
return NextResponse.json({ workspace: updated })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function DELETE(req: Request) {
|
|
40
|
+
const { searchParams } = new URL(req.url)
|
|
41
|
+
const id = searchParams.get('id')
|
|
42
|
+
if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 })
|
|
43
|
+
const removed = deleteWorkspace(id)
|
|
44
|
+
if (!removed) return NextResponse.json({ error: 'cannot delete default or unknown workspace' }, { status: 400 })
|
|
45
|
+
return NextResponse.json({ ok: true })
|
|
46
|
+
}
|