@swarmclawai/swarmclaw 0.5.2 → 0.6.0
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 +42 -7
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +4 -2
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/[id]/route.ts +27 -0
- package/src/app/api/notifications/route.ts +68 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +155 -0
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- package/src/app/api/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +20 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/route.ts +1 -0
- package/src/app/api/usage/route.ts +45 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +58 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +42 -0
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +32 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +48 -15
- package/src/components/agents/agent-chat-list.tsx +123 -10
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +56 -63
- package/src/components/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/chat/activity-moment.tsx +169 -0
- package/src/components/chat/chat-header.tsx +2 -0
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/file-path-chip.tsx +125 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +46 -295
- package/src/components/chat/message-list.tsx +50 -1
- package/src/components/chat/streaming-bubble.tsx +36 -46
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +66 -70
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +123 -0
- package/src/components/chatrooms/chatroom-message.tsx +427 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-sheet.tsx +34 -47
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +79 -41
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +209 -83
- package/src/components/layout/mobile-header.tsx +2 -0
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +3 -2
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +25 -25
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +223 -0
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +296 -0
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +39 -0
- package/src/components/shared/settings/settings-page.tsx +180 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +46 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +89 -72
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +78 -0
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-view-router.ts +69 -19
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/cron-human.ts +114 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/server/chat-execution.ts +24 -4
- package/src/lib/server/connectors/manager.ts +11 -0
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +42 -0
- package/src/lib/server/daemon-state.ts +165 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +40 -5
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/memory-consolidation.ts +92 -0
- package/src/lib/server/memory-db.ts +51 -6
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +5 -4
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +6 -1
- package/src/lib/server/storage.ts +80 -29
- package/src/lib/server/stream-agent-chat.ts +153 -47
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/proxy.ts +79 -2
- package/src/stores/use-app-store.ts +94 -3
- package/src/stores/use-chat-store.ts +48 -3
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +69 -2
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadTasks, saveTasks, logActivity } from '@/lib/server/storage'
|
|
3
|
+
import { enqueueTask, disableSessionHeartbeat } from '@/lib/server/queue'
|
|
4
|
+
import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
|
|
5
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
6
|
+
import { createNotification } from '@/lib/server/create-notification'
|
|
7
|
+
import type { BoardTaskStatus } from '@/types'
|
|
8
|
+
|
|
9
|
+
const VALID_STATUSES: BoardTaskStatus[] = ['backlog', 'queued', 'running', 'completed', 'failed', 'archived']
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Bulk update tasks — batch status changes, agent/project reassignment, or archive/delete.
|
|
13
|
+
*
|
|
14
|
+
* POST body:
|
|
15
|
+
* ids: string[] — required, task IDs to update
|
|
16
|
+
* status?: BoardTaskStatus — move all to this status
|
|
17
|
+
* agentId?: string | null — reassign agent (null to clear)
|
|
18
|
+
* projectId?: string | null — reassign project (null to clear)
|
|
19
|
+
*/
|
|
20
|
+
export async function POST(req: Request) {
|
|
21
|
+
const body = await req.json()
|
|
22
|
+
const ids: unknown = body.ids
|
|
23
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
24
|
+
return NextResponse.json({ error: 'ids must be a non-empty array' }, { status: 400 })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const taskIds = ids.filter((id): id is string => typeof id === 'string')
|
|
28
|
+
if (taskIds.length === 0) {
|
|
29
|
+
return NextResponse.json({ error: 'No valid task IDs provided' }, { status: 400 })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const tasks = loadTasks()
|
|
33
|
+
let updated = 0
|
|
34
|
+
const results: string[] = []
|
|
35
|
+
|
|
36
|
+
for (const id of taskIds) {
|
|
37
|
+
if (!tasks[id]) continue
|
|
38
|
+
const prevStatus = tasks[id].status
|
|
39
|
+
|
|
40
|
+
if (typeof body.status === 'string' && VALID_STATUSES.includes(body.status as BoardTaskStatus)) {
|
|
41
|
+
tasks[id].status = body.status as BoardTaskStatus
|
|
42
|
+
if (body.status === 'archived' && prevStatus !== 'archived') {
|
|
43
|
+
tasks[id].archivedAt = Date.now()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if ('agentId' in body) {
|
|
48
|
+
tasks[id].agentId = body.agentId === null ? '' : String(body.agentId)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if ('projectId' in body) {
|
|
52
|
+
if (body.projectId === null) {
|
|
53
|
+
delete tasks[id].projectId
|
|
54
|
+
} else {
|
|
55
|
+
tasks[id].projectId = String(body.projectId)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
tasks[id].updatedAt = Date.now()
|
|
60
|
+
updated++
|
|
61
|
+
results.push(id)
|
|
62
|
+
|
|
63
|
+
// Side-effects for status transitions
|
|
64
|
+
if (prevStatus !== tasks[id].status) {
|
|
65
|
+
logActivity({
|
|
66
|
+
entityType: 'task',
|
|
67
|
+
entityId: id,
|
|
68
|
+
action: 'updated',
|
|
69
|
+
actor: 'user',
|
|
70
|
+
summary: `Bulk update: "${tasks[id].title}" (${prevStatus} → ${tasks[id].status})`,
|
|
71
|
+
})
|
|
72
|
+
pushMainLoopEventToMainSessions({
|
|
73
|
+
type: 'task_status_changed',
|
|
74
|
+
text: `Task "${tasks[id].title}" (${id}) moved ${prevStatus} → ${tasks[id].status}.`,
|
|
75
|
+
})
|
|
76
|
+
if (tasks[id].status === 'completed' || tasks[id].status === 'failed') {
|
|
77
|
+
disableSessionHeartbeat(tasks[id].sessionId)
|
|
78
|
+
}
|
|
79
|
+
if (prevStatus !== 'queued' && tasks[id].status === 'queued') {
|
|
80
|
+
enqueueTask(id)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
saveTasks(tasks)
|
|
86
|
+
|
|
87
|
+
if (updated > 0) {
|
|
88
|
+
const action = body.status
|
|
89
|
+
? `moved ${updated} task(s) to ${body.status}`
|
|
90
|
+
: `updated ${updated} task(s)`
|
|
91
|
+
createNotification({
|
|
92
|
+
type: 'success',
|
|
93
|
+
title: `Bulk update: ${action}`,
|
|
94
|
+
entityType: 'task',
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
notify('tasks')
|
|
99
|
+
return NextResponse.json({ updated, ids: results })
|
|
100
|
+
}
|
|
@@ -70,6 +70,7 @@ export async function POST(req: Request) {
|
|
|
70
70
|
description: body.description || '',
|
|
71
71
|
status: body.status || 'backlog',
|
|
72
72
|
agentId: body.agentId || '',
|
|
73
|
+
projectId: typeof body.projectId === 'string' && body.projectId ? body.projectId : null,
|
|
73
74
|
goalContract: body.goalContract || null,
|
|
74
75
|
cwd: typeof body.cwd === 'string' ? body.cwd : null,
|
|
75
76
|
file: typeof body.file === 'string' ? body.file : null,
|
|
@@ -78,6 +78,50 @@ export async function GET(req: Request) {
|
|
|
78
78
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
79
79
|
.map(([bucket, data]) => ({ bucket, tokens: data.tokens, cost: Math.round(data.cost * 10000) / 10000 }))
|
|
80
80
|
|
|
81
|
+
// Provider health stats
|
|
82
|
+
const healthAccum: Record<string, {
|
|
83
|
+
totalRequests: number
|
|
84
|
+
successCount: number
|
|
85
|
+
errorCount: number
|
|
86
|
+
lastUsed: number
|
|
87
|
+
models: Set<string>
|
|
88
|
+
}> = {}
|
|
89
|
+
|
|
90
|
+
for (const r of records) {
|
|
91
|
+
const prov = r.provider || 'unknown'
|
|
92
|
+
if (!healthAccum[prov]) {
|
|
93
|
+
healthAccum[prov] = { totalRequests: 0, successCount: 0, errorCount: 0, lastUsed: 0, models: new Set() }
|
|
94
|
+
}
|
|
95
|
+
const h = healthAccum[prov]
|
|
96
|
+
h.totalRequests += 1
|
|
97
|
+
// UsageRecord has no error/status field — logged records are successful completions
|
|
98
|
+
h.successCount += 1
|
|
99
|
+
if ((r.timestamp || 0) > h.lastUsed) h.lastUsed = r.timestamp || 0
|
|
100
|
+
if (r.model) h.models.add(r.model)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const providerHealth: Record<string, {
|
|
104
|
+
totalRequests: number
|
|
105
|
+
successCount: number
|
|
106
|
+
errorCount: number
|
|
107
|
+
errorRate: number
|
|
108
|
+
avgLatencyMs: number
|
|
109
|
+
lastUsed: number
|
|
110
|
+
models: string[]
|
|
111
|
+
}> = {}
|
|
112
|
+
|
|
113
|
+
for (const [prov, h] of Object.entries(healthAccum)) {
|
|
114
|
+
providerHealth[prov] = {
|
|
115
|
+
totalRequests: h.totalRequests,
|
|
116
|
+
successCount: h.successCount,
|
|
117
|
+
errorCount: h.errorCount,
|
|
118
|
+
errorRate: h.totalRequests > 0 ? h.errorCount / h.totalRequests : 0,
|
|
119
|
+
avgLatencyMs: 0, // UsageRecord does not track latency
|
|
120
|
+
lastUsed: h.lastUsed,
|
|
121
|
+
models: Array.from(h.models),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
81
125
|
return NextResponse.json({
|
|
82
126
|
records,
|
|
83
127
|
totalTokens,
|
|
@@ -85,5 +129,6 @@ export async function GET(req: Request) {
|
|
|
85
129
|
byAgent,
|
|
86
130
|
byProvider,
|
|
87
131
|
timeSeries,
|
|
132
|
+
providerHealth,
|
|
88
133
|
})
|
|
89
134
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { genId } from '@/lib/id'
|
|
2
|
+
import { timingSafeEqual } from 'node:crypto'
|
|
2
3
|
import { NextResponse } from 'next/server'
|
|
3
4
|
import { loadAgents, loadSessions, loadWebhooks, saveSessions, saveWebhooks, appendWebhookLog, upsertWebhookRetry } from '@/lib/server/storage'
|
|
4
5
|
import { WORKSPACE_DIR } from '@/lib/server/data-dir'
|
|
5
6
|
import { enqueueSessionRun } from '@/lib/server/session-run-manager'
|
|
7
|
+
import { enqueueSystemEvent } from '@/lib/server/system-events'
|
|
8
|
+
import { requestHeartbeatNow } from '@/lib/server/heartbeat-wake'
|
|
6
9
|
import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
|
|
7
10
|
import type { WebhookRetryEntry } from '@/types'
|
|
8
11
|
|
|
@@ -71,7 +74,12 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
71
74
|
if (secret) {
|
|
72
75
|
const url = new URL(req.url)
|
|
73
76
|
const provided = req.headers.get('x-webhook-secret') || url.searchParams.get('secret') || ''
|
|
74
|
-
|
|
77
|
+
const secretBuf = Buffer.from(secret)
|
|
78
|
+
const providedBuf = Buffer.from(provided)
|
|
79
|
+
// timingSafeEqual requires equal lengths; compare against secretBuf if lengths differ
|
|
80
|
+
const compareBuf = providedBuf.length === secretBuf.length ? providedBuf : secretBuf
|
|
81
|
+
const isInvalid = providedBuf.length !== secretBuf.length || !timingSafeEqual(secretBuf, compareBuf)
|
|
82
|
+
if (isInvalid) {
|
|
75
83
|
appendWebhookLog(genId(8), {
|
|
76
84
|
id: genId(8), webhookId: id, event: 'unknown',
|
|
77
85
|
payload: '', status: 'error', error: 'Invalid webhook secret', timestamp: Date.now(),
|
|
@@ -193,6 +201,12 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
193
201
|
mode: 'followup',
|
|
194
202
|
})
|
|
195
203
|
|
|
204
|
+
// Enqueue system event + heartbeat wake
|
|
205
|
+
enqueueSystemEvent(sid, `Webhook received: ${webhook.name || id} (${incomingEvent})`)
|
|
206
|
+
if (webhook.agentId) {
|
|
207
|
+
requestHeartbeatNow({ agentId: webhook.agentId, reason: 'webhook' })
|
|
208
|
+
}
|
|
209
|
+
|
|
196
210
|
appendWebhookLog(genId(8), {
|
|
197
211
|
id: genId(8), webhookId: id, event: incomingEvent,
|
|
198
212
|
payload: (rawBody || '').slice(0, 2000), status: 'success',
|
package/src/app/globals.css
CHANGED
|
@@ -49,16 +49,17 @@
|
|
|
49
49
|
--radius-4xl: calc(var(--radius) + 16px);
|
|
50
50
|
|
|
51
51
|
/* ===== Midnight Glass Palette ===== */
|
|
52
|
-
--
|
|
53
|
-
--color-
|
|
54
|
-
--color-
|
|
55
|
-
--color-surface
|
|
56
|
-
--color-surface-
|
|
52
|
+
/* Surfaces derived from --neutral-tint for single-knob theming */
|
|
53
|
+
--color-bg: color-mix(in srgb, var(--neutral-tint, #1e1e30) 40%, #000);
|
|
54
|
+
--color-raised: color-mix(in srgb, var(--neutral-tint, #1e1e30) 50%, #000);
|
|
55
|
+
--color-surface: color-mix(in srgb, var(--neutral-tint, #1e1e30) 65%, #000);
|
|
56
|
+
--color-surface-2: var(--neutral-tint, #1e1e30);
|
|
57
|
+
--color-surface-3: color-mix(in srgb, var(--neutral-tint, #1e1e30) 80%, #333);
|
|
57
58
|
--color-border-hi: rgba(255,255,255,0.07);
|
|
58
59
|
--color-border-focus: rgba(99,102,241,0.5);
|
|
59
60
|
--color-text: #e2e2ec;
|
|
60
|
-
--color-text-2: #
|
|
61
|
-
--color-text-3: #
|
|
61
|
+
--color-text-2: #a0a0ba;
|
|
62
|
+
--color-text-3: #8282a0;
|
|
62
63
|
--color-accent-soft: rgba(99,102,241,0.08);
|
|
63
64
|
--color-accent-glow: rgba(99,102,241,0.18);
|
|
64
65
|
--color-accent-bright: #818CF8;
|
|
@@ -83,25 +84,29 @@
|
|
|
83
84
|
--font-sora: 'Segoe UI';
|
|
84
85
|
--font-jetbrains-mono: 'SF Mono';
|
|
85
86
|
--radius: 0.625rem;
|
|
86
|
-
|
|
87
|
+
|
|
88
|
+
/* ===== Single-Tint Theming ===== */
|
|
89
|
+
/* Change this one value to shift the entire palette hue */
|
|
90
|
+
--neutral-tint: #1e1e30;
|
|
91
|
+
--background: color-mix(in srgb, var(--neutral-tint) 40%, #000);
|
|
87
92
|
--foreground: #e2e2ec;
|
|
88
|
-
--card: #
|
|
93
|
+
--card: color-mix(in srgb, var(--neutral-tint) 50%, #000);
|
|
89
94
|
--card-foreground: #e2e2ec;
|
|
90
|
-
--popover: #
|
|
95
|
+
--popover: color-mix(in srgb, var(--neutral-tint) 50%, #000);
|
|
91
96
|
--popover-foreground: #e2e2ec;
|
|
92
97
|
--primary: #6366F1;
|
|
93
98
|
--primary-foreground: #ffffff;
|
|
94
|
-
--secondary: #
|
|
99
|
+
--secondary: color-mix(in srgb, var(--neutral-tint) 65%, #000);
|
|
95
100
|
--secondary-foreground: #e2e2ec;
|
|
96
|
-
--muted: #
|
|
97
|
-
--muted-foreground: #
|
|
101
|
+
--muted: color-mix(in srgb, var(--neutral-tint) 65%, #000);
|
|
102
|
+
--muted-foreground: #a0a0ba;
|
|
98
103
|
--accent: #6366F1;
|
|
99
104
|
--accent-foreground: #ffffff;
|
|
100
105
|
--destructive: #F43F5E;
|
|
101
106
|
--border: rgba(255,255,255,0.04);
|
|
102
107
|
--input: rgba(255,255,255,0.04);
|
|
103
108
|
--ring: rgba(99,102,241,0.4);
|
|
104
|
-
--sidebar: #
|
|
109
|
+
--sidebar: color-mix(in srgb, var(--neutral-tint) 50%, #000);
|
|
105
110
|
--sidebar-foreground: #e2e2ec;
|
|
106
111
|
--sidebar-primary: #6366F1;
|
|
107
112
|
--sidebar-primary-foreground: #ffffff;
|
|
@@ -113,7 +118,7 @@
|
|
|
113
118
|
/* ===== Status Badge System ===== */
|
|
114
119
|
--status-idle-bg: rgba(255,255,255,0.04);
|
|
115
120
|
--status-idle-border: rgba(255,255,255,0.06);
|
|
116
|
-
--status-idle-fg: #
|
|
121
|
+
--status-idle-fg: #a0a0ba;
|
|
117
122
|
--status-running-bg: rgba(52,211,153,0.08);
|
|
118
123
|
--status-running-border: rgba(52,211,153,0.15);
|
|
119
124
|
--status-running-fg: #34D399;
|
|
@@ -205,6 +210,10 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
|
|
|
205
210
|
from { opacity: 0; transform: scale(0.97) translateY(6px); }
|
|
206
211
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
|
207
212
|
}
|
|
213
|
+
@keyframes msg-in {
|
|
214
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
215
|
+
to { opacity: 1; transform: translateY(0); }
|
|
216
|
+
}
|
|
208
217
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
209
218
|
@keyframes slide-in-left {
|
|
210
219
|
from { transform: translateX(-100%); }
|
|
@@ -243,6 +252,30 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
|
|
|
243
252
|
to { opacity: 1; transform: translateY(0); }
|
|
244
253
|
}
|
|
245
254
|
|
|
255
|
+
/* Heartbeat float animation */
|
|
256
|
+
@keyframes heartbeat-float {
|
|
257
|
+
0% { opacity: 1; transform: translateY(0) scale(1); }
|
|
258
|
+
50% { opacity: 0.7; transform: translateY(-14px) scale(1.15); }
|
|
259
|
+
100% { opacity: 0; transform: translateY(-26px) scale(0.85); }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* Activity moment animations */
|
|
263
|
+
@keyframes activity-moment-in {
|
|
264
|
+
from { opacity: 0; transform: translateY(6px) scale(0.92); }
|
|
265
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
266
|
+
}
|
|
267
|
+
@keyframes activity-moment-out {
|
|
268
|
+
from { opacity: 1; transform: translateY(0) scale(1); }
|
|
269
|
+
to { opacity: 0; transform: translateY(-8px) scale(0.95); }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
@keyframes avatar-moment-pulse {
|
|
273
|
+
0% { transform: scale(1); }
|
|
274
|
+
30% { transform: scale(1.15); }
|
|
275
|
+
60% { transform: scale(0.95); }
|
|
276
|
+
100% { transform: scale(1); }
|
|
277
|
+
}
|
|
278
|
+
|
|
246
279
|
/* AI avatar mood animations */
|
|
247
280
|
@keyframes ai-pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.15); } }
|
|
248
281
|
@keyframes ai-glow { 0%,100% { box-shadow: 0 0 0 0 rgba(99,102,241,0); } 50% { box-shadow: 0 0 12px 4px rgba(99,102,241,0.35); } }
|
|
@@ -414,6 +447,16 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
|
|
|
414
447
|
font-family: var(--font-sora), 'Sora', system-ui, sans-serif;
|
|
415
448
|
}
|
|
416
449
|
|
|
450
|
+
/* Monospace micro-label utility */
|
|
451
|
+
.label-mono {
|
|
452
|
+
font-family: var(--font-mono);
|
|
453
|
+
font-size: 10px;
|
|
454
|
+
font-weight: 600;
|
|
455
|
+
letter-spacing: 0.06em;
|
|
456
|
+
text-transform: uppercase;
|
|
457
|
+
color: var(--color-text-3);
|
|
458
|
+
}
|
|
459
|
+
|
|
417
460
|
/* ===== Status Badge Classes ===== */
|
|
418
461
|
.status-badge-idle, .status-badge-running, .status-badge-error,
|
|
419
462
|
.status-badge-connecting, .status-badge-connected, .status-badge-disconnected,
|
package/src/app/page.tsx
CHANGED
|
@@ -12,6 +12,130 @@ import { SetupWizard } from '@/components/auth/setup-wizard'
|
|
|
12
12
|
import { AppLayout } from '@/components/layout/app-layout'
|
|
13
13
|
import { useViewRouter } from '@/hooks/use-view-router'
|
|
14
14
|
|
|
15
|
+
function FullScreenLoader() {
|
|
16
|
+
return (
|
|
17
|
+
<div className="h-full flex flex-col items-center justify-center bg-bg overflow-hidden select-none">
|
|
18
|
+
{/* Animated orbital ring */}
|
|
19
|
+
<div className="relative w-[120px] h-[120px] mb-8">
|
|
20
|
+
{/* Outer glow pulse */}
|
|
21
|
+
<div
|
|
22
|
+
className="absolute inset-[-20px] rounded-full"
|
|
23
|
+
style={{
|
|
24
|
+
background: 'radial-gradient(circle, rgba(99,102,241,0.08) 0%, transparent 70%)',
|
|
25
|
+
animation: 'sc-glow 2.5s ease-in-out infinite',
|
|
26
|
+
}}
|
|
27
|
+
/>
|
|
28
|
+
|
|
29
|
+
{/* Orbital ring */}
|
|
30
|
+
<div
|
|
31
|
+
className="absolute inset-0 rounded-full border border-white/[0.06]"
|
|
32
|
+
style={{ animation: 'sc-ring 3s linear infinite' }}
|
|
33
|
+
/>
|
|
34
|
+
|
|
35
|
+
{/* Orbiting dots */}
|
|
36
|
+
{[0, 1, 2, 3, 4, 5].map((i) => (
|
|
37
|
+
<div
|
|
38
|
+
key={i}
|
|
39
|
+
className="absolute inset-0"
|
|
40
|
+
style={{
|
|
41
|
+
animation: `sc-orbit 2.4s cubic-bezier(0.4, 0, 0.2, 1) infinite`,
|
|
42
|
+
animationDelay: `${i * -0.4}s`,
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<div
|
|
46
|
+
className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full"
|
|
47
|
+
style={{
|
|
48
|
+
width: i === 0 ? 8 : 6,
|
|
49
|
+
height: i === 0 ? 8 : 6,
|
|
50
|
+
background: i === 0 ? '#818CF8' : `rgba(129, 140, 248, ${0.7 - i * 0.1})`,
|
|
51
|
+
boxShadow: i === 0 ? '0 0 12px rgba(99,102,241,0.5)' : 'none',
|
|
52
|
+
}}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
))}
|
|
56
|
+
|
|
57
|
+
{/* Center logo mark */}
|
|
58
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
59
|
+
<div
|
|
60
|
+
className="relative"
|
|
61
|
+
style={{ animation: 'sc-breathe 2.5s ease-in-out infinite' }}
|
|
62
|
+
>
|
|
63
|
+
<svg width="36" height="36" viewBox="0 0 36 36" fill="none">
|
|
64
|
+
{/* Hexagonal claw mark */}
|
|
65
|
+
<path
|
|
66
|
+
d="M18 4L30 11V25L18 32L6 25V11L18 4Z"
|
|
67
|
+
stroke="rgba(129, 140, 248, 0.3)"
|
|
68
|
+
strokeWidth="1"
|
|
69
|
+
fill="none"
|
|
70
|
+
/>
|
|
71
|
+
<path
|
|
72
|
+
d="M18 9L25 13V23L18 27L11 23V13L18 9Z"
|
|
73
|
+
stroke="rgba(129, 140, 248, 0.5)"
|
|
74
|
+
strokeWidth="1.5"
|
|
75
|
+
fill="rgba(99, 102, 241, 0.06)"
|
|
76
|
+
/>
|
|
77
|
+
{/* Claw lines */}
|
|
78
|
+
<path d="M14 15L18 20L22 15" stroke="#818CF8" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
79
|
+
<path d="M12 13L18 20L24 13" stroke="rgba(129, 140, 248, 0.3)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
80
|
+
</svg>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Brand text */}
|
|
86
|
+
<div
|
|
87
|
+
className="text-[15px] font-display font-700 tracking-[0.15em] uppercase"
|
|
88
|
+
style={{
|
|
89
|
+
background: 'linear-gradient(135deg, rgba(255,255,255,0.6), rgba(129, 140, 248, 0.8))',
|
|
90
|
+
WebkitBackgroundClip: 'text',
|
|
91
|
+
WebkitTextFillColor: 'transparent',
|
|
92
|
+
animation: 'sc-text-fade 2s ease-in-out infinite alternate',
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
SwarmClaw
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{/* Loading bar */}
|
|
99
|
+
<div className="mt-4 w-[100px] h-[2px] rounded-full bg-white/[0.06] overflow-hidden">
|
|
100
|
+
<div
|
|
101
|
+
className="h-full rounded-full bg-accent-bright/60"
|
|
102
|
+
style={{ animation: 'sc-progress 1.5s ease-in-out infinite' }}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{/* Loading animation keyframes */}
|
|
107
|
+
<style>{`
|
|
108
|
+
@keyframes sc-orbit {
|
|
109
|
+
from { transform: rotate(0deg); }
|
|
110
|
+
to { transform: rotate(360deg); }
|
|
111
|
+
}
|
|
112
|
+
@keyframes sc-ring {
|
|
113
|
+
from { transform: rotate(0deg) scale(1); }
|
|
114
|
+
50% { transform: rotate(180deg) scale(1.02); }
|
|
115
|
+
to { transform: rotate(360deg) scale(1); }
|
|
116
|
+
}
|
|
117
|
+
@keyframes sc-breathe {
|
|
118
|
+
0%, 100% { transform: scale(1); opacity: 0.9; }
|
|
119
|
+
50% { transform: scale(1.06); opacity: 1; }
|
|
120
|
+
}
|
|
121
|
+
@keyframes sc-glow {
|
|
122
|
+
0%, 100% { opacity: 0.5; transform: scale(0.9); }
|
|
123
|
+
50% { opacity: 1; transform: scale(1.1); }
|
|
124
|
+
}
|
|
125
|
+
@keyframes sc-text-fade {
|
|
126
|
+
0% { opacity: 0.6; }
|
|
127
|
+
100% { opacity: 1; }
|
|
128
|
+
}
|
|
129
|
+
@keyframes sc-progress {
|
|
130
|
+
0% { width: 0; margin-left: 0; }
|
|
131
|
+
50% { width: 70%; margin-left: 15%; }
|
|
132
|
+
100% { width: 0; margin-left: 100%; }
|
|
133
|
+
}
|
|
134
|
+
`}</style>
|
|
135
|
+
</div>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
15
139
|
export default function Home() {
|
|
16
140
|
const currentUser = useAppStore((s) => s.currentUser)
|
|
17
141
|
const setUser = useAppStore((s) => s.setUser)
|
|
@@ -83,26 +207,31 @@ export default function Home() {
|
|
|
83
207
|
|
|
84
208
|
useWs('sessions', loadSessions, 5000)
|
|
85
209
|
|
|
86
|
-
// Auto-select
|
|
210
|
+
// Auto-select agent's thread on load — resolves a persisted agentId into a session,
|
|
211
|
+
// or falls back to defaultAgentId from settings, then first agent.
|
|
212
|
+
const [agentReady, setAgentReady] = useState(false)
|
|
87
213
|
useEffect(() => {
|
|
88
214
|
if (!authenticated || !currentUser) return
|
|
89
|
-
const state = useAppStore.getState()
|
|
90
|
-
// Only auto-select if no agent is selected yet
|
|
91
|
-
if (state.currentAgentId) return
|
|
92
|
-
|
|
93
|
-
// Load agents and select 'default' agent
|
|
94
215
|
let cancelled = false
|
|
95
216
|
;(async () => {
|
|
96
217
|
try {
|
|
218
|
+
const state = useAppStore.getState()
|
|
97
219
|
await state.loadAgents()
|
|
98
220
|
if (cancelled) return
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
221
|
+
|
|
222
|
+
const { agents, currentAgentId, appSettings } = useAppStore.getState()
|
|
223
|
+
// Priority: persisted agent > settings default > first agent
|
|
224
|
+
const targetId = (currentAgentId && agents[currentAgentId])
|
|
225
|
+
? currentAgentId
|
|
226
|
+
: (appSettings.defaultAgentId && agents[appSettings.defaultAgentId])
|
|
227
|
+
? appSettings.defaultAgentId
|
|
228
|
+
: Object.values(agents)[0]?.id || null
|
|
229
|
+
|
|
230
|
+
if (targetId) {
|
|
231
|
+
await useAppStore.getState().setCurrentAgent(targetId)
|
|
104
232
|
}
|
|
105
233
|
} catch { /* ignore */ }
|
|
234
|
+
if (!cancelled) setAgentReady(true)
|
|
106
235
|
})()
|
|
107
236
|
return () => { cancelled = true }
|
|
108
237
|
}, [authenticated, currentUser])
|
|
@@ -148,10 +277,10 @@ export default function Home() {
|
|
|
148
277
|
|
|
149
278
|
useViewRouter()
|
|
150
279
|
|
|
151
|
-
if (!hydrated || !authChecked) return
|
|
280
|
+
if (!hydrated || !authChecked) return <FullScreenLoader />
|
|
152
281
|
if (!authenticated) return <AccessKeyGate onAuthenticated={() => setAuthenticated(true)} />
|
|
153
282
|
if (!currentUser) return <UserPicker />
|
|
154
|
-
if (setupDone === null) return
|
|
283
|
+
if (setupDone === null || !agentReady) return <FullScreenLoader />
|
|
155
284
|
if (!setupDone) return <SetupWizard onComplete={() => setSetupDone(true)} />
|
|
156
285
|
return <AppLayout />
|
|
157
286
|
}
|
package/src/cli/index.js
CHANGED
|
@@ -56,6 +56,29 @@ const COMMAND_GROUPS = [
|
|
|
56
56
|
cmd('install', 'POST', '/clawhub/install', 'Install a skill from ClawHub', { expectsJsonBody: true }),
|
|
57
57
|
],
|
|
58
58
|
},
|
|
59
|
+
{
|
|
60
|
+
name: 'chatrooms',
|
|
61
|
+
description: 'Manage multi-agent chatrooms',
|
|
62
|
+
commands: [
|
|
63
|
+
cmd('list', 'GET', '/chatrooms', 'List chatrooms'),
|
|
64
|
+
cmd('get', 'GET', '/chatrooms/:id', 'Get chatroom by id'),
|
|
65
|
+
cmd('create', 'POST', '/chatrooms', 'Create a chatroom', { expectsJsonBody: true }),
|
|
66
|
+
cmd('update', 'PUT', '/chatrooms/:id', 'Update a chatroom', { expectsJsonBody: true }),
|
|
67
|
+
cmd('delete', 'DELETE', '/chatrooms/:id', 'Delete a chatroom'),
|
|
68
|
+
cmd('chat', 'POST', '/chatrooms/:id/chat', 'Post a message to a chatroom and stream agent replies', {
|
|
69
|
+
expectsJsonBody: true,
|
|
70
|
+
responseType: 'sse',
|
|
71
|
+
}),
|
|
72
|
+
cmd('add-member', 'POST', '/chatrooms/:id/members', 'Add an agent to a chatroom (use --data \'{"agentId":"..."}\')', { expectsJsonBody: true }),
|
|
73
|
+
cmd('remove-member', 'DELETE', '/chatrooms/:id/members', 'Remove an agent from a chatroom (use --data \'{"agentId":"..."}\')', { expectsJsonBody: true }),
|
|
74
|
+
cmd('react', 'POST', '/chatrooms/:id/reactions', 'Toggle a reaction on a chatroom message', {
|
|
75
|
+
expectsJsonBody: true,
|
|
76
|
+
}),
|
|
77
|
+
cmd('pin', 'POST', '/chatrooms/:id/pins', 'Toggle pin on a chatroom message', {
|
|
78
|
+
expectsJsonBody: true,
|
|
79
|
+
}),
|
|
80
|
+
],
|
|
81
|
+
},
|
|
59
82
|
{
|
|
60
83
|
name: 'connectors',
|
|
61
84
|
description: 'Manage chat connectors',
|
|
@@ -183,6 +206,17 @@ const COMMAND_GROUPS = [
|
|
|
183
206
|
cmd('get', 'GET', '/memory-images/:filename', 'Download memory image by filename', { responseType: 'binary' }),
|
|
184
207
|
],
|
|
185
208
|
},
|
|
209
|
+
{
|
|
210
|
+
name: 'notifications',
|
|
211
|
+
description: 'Manage in-app notifications',
|
|
212
|
+
commands: [
|
|
213
|
+
cmd('list', 'GET', '/notifications', 'List notifications (use --query unreadOnly=true --query limit=100)'),
|
|
214
|
+
cmd('create', 'POST', '/notifications', 'Create notification', { expectsJsonBody: true }),
|
|
215
|
+
cmd('clear', 'DELETE', '/notifications', 'Clear read notifications'),
|
|
216
|
+
cmd('mark-read', 'PUT', '/notifications/:id', 'Mark notification as read'),
|
|
217
|
+
cmd('delete', 'DELETE', '/notifications/:id', 'Delete notification by id'),
|
|
218
|
+
],
|
|
219
|
+
},
|
|
186
220
|
{
|
|
187
221
|
name: 'mcp-servers',
|
|
188
222
|
description: 'Manage MCP server configurations',
|
|
@@ -293,6 +327,13 @@ const COMMAND_GROUPS = [
|
|
|
293
327
|
cmd('models-clear', 'DELETE', '/providers/:id/models', 'Clear provider model overrides'),
|
|
294
328
|
],
|
|
295
329
|
},
|
|
330
|
+
{
|
|
331
|
+
name: 'search',
|
|
332
|
+
description: 'Global search across app resources',
|
|
333
|
+
commands: [
|
|
334
|
+
cmd('query', 'GET', '/search', 'Search agents/tasks/sessions/schedules/webhooks/skills (use --query q=term)'),
|
|
335
|
+
],
|
|
336
|
+
},
|
|
296
337
|
{
|
|
297
338
|
name: 'runs',
|
|
298
339
|
description: 'Session run queue/history',
|
|
@@ -409,6 +450,7 @@ const COMMAND_GROUPS = [
|
|
|
409
450
|
cmd('list', 'GET', '/tasks', 'List tasks'),
|
|
410
451
|
cmd('get', 'GET', '/tasks/:id', 'Get task'),
|
|
411
452
|
cmd('create', 'POST', '/tasks', 'Create task', { expectsJsonBody: true }),
|
|
453
|
+
cmd('bulk', 'POST', '/tasks/bulk', 'Bulk update tasks (status/agent/project)', { expectsJsonBody: true }),
|
|
412
454
|
cmd('update', 'PUT', '/tasks/:id', 'Update task', { expectsJsonBody: true }),
|
|
413
455
|
cmd('delete', 'DELETE', '/tasks/:id', 'Delete task'),
|
|
414
456
|
cmd('purge', 'DELETE', '/tasks', 'Bulk delete tasks', { expectsJsonBody: true }),
|
package/src/cli/index.test.js
CHANGED
|
@@ -81,6 +81,36 @@ test('CLI command map covers all API route method/path pairs', () => {
|
|
|
81
81
|
assert.deepEqual(missing, [])
|
|
82
82
|
})
|
|
83
83
|
|
|
84
|
+
test('Binary CLI router reaches every mapped API command pair', async () => {
|
|
85
|
+
const { shouldUseLegacyTsCli, TS_CLI_ACTIONS } = await import('../../bin/swarmclaw.js')
|
|
86
|
+
|
|
87
|
+
for (const command of COMMANDS) {
|
|
88
|
+
if (command.virtual) continue
|
|
89
|
+
|
|
90
|
+
const pathArgs = extractPathParams(command.route).map((name, index) => `${name}-${index + 1}`)
|
|
91
|
+
const routedToLegacyTs = shouldUseLegacyTsCli([command.group, command.action, ...pathArgs])
|
|
92
|
+
|
|
93
|
+
if (routedToLegacyTs) {
|
|
94
|
+
assert.ok(
|
|
95
|
+
TS_CLI_ACTIONS[command.group]?.has(command.action),
|
|
96
|
+
`legacy TS router should only claim known actions (${command.group} ${command.action})`,
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Spot-check known API commands that are map-only today.
|
|
102
|
+
assert.equal(shouldUseLegacyTsCli(['chatrooms', 'list']), false)
|
|
103
|
+
assert.equal(shouldUseLegacyTsCli(['tasks', 'approve', 'task-1']), false)
|
|
104
|
+
|
|
105
|
+
// Help paths should route to mapped CLI for full command discoverability.
|
|
106
|
+
assert.equal(shouldUseLegacyTsCli([]), false)
|
|
107
|
+
assert.equal(shouldUseLegacyTsCli(['--help']), false)
|
|
108
|
+
assert.equal(shouldUseLegacyTsCli(['tasks', '--help']), false)
|
|
109
|
+
|
|
110
|
+
// And a legacy command that should remain on the richer TS path.
|
|
111
|
+
assert.equal(shouldUseLegacyTsCli(['tasks', 'create']), true)
|
|
112
|
+
})
|
|
113
|
+
|
|
84
114
|
test('parseArgv parses group/action/options', () => {
|
|
85
115
|
const parsed = parseArgv([
|
|
86
116
|
'runs',
|