@swarmclawai/swarmclaw 0.7.1 → 0.7.3
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 +155 -150
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +37 -9
- package/src/app/api/agents/route.ts +13 -2
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
- package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
- package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
- package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
- package/src/app/api/{sessions → chats}/route.ts +21 -7
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/skills/route.ts +11 -3
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +6 -26
- package/src/app/api/plugins/settings/route.ts +40 -0
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/settings/route.ts +49 -7
- package/src/app/api/tasks/[id]/route.ts +15 -6
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/usage/route.ts +30 -0
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +39 -33
- package/src/cli/index.ts +43 -49
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +16 -13
- package/src/components/agents/agent-chat-list.tsx +104 -4
- package/src/components/agents/agent-list.tsx +54 -22
- package/src/components/agents/agent-sheet.tsx +209 -18
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +110 -50
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +39 -27
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
- package/src/components/chat/chat-header.tsx +299 -314
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +5 -3
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- package/src/components/chatrooms/chatroom-view.tsx +347 -205
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +218 -1
- package/src/components/home/home-view.tsx +129 -5
- package/src/components/layout/app-layout.tsx +392 -182
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +487 -254
- package/src/components/plugins/plugin-sheet.tsx +236 -13
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -25
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +78 -1
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +244 -56
- package/src/components/tasks/approvals-panel.tsx +205 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/usage/metrics-dashboard.tsx +147 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +8 -8
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/providers/claude-cli.ts +1 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +205 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +36 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/capability-router.ts +10 -8
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
- package/src/lib/server/chat-execution.ts +285 -165
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +67 -2
- package/src/lib/server/chatroom-helpers.ts +48 -8
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +948 -112
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +188 -9
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +61 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/heartbeat-service.ts +14 -40
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +28 -1103
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +5 -6
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +20 -9
- package/src/lib/server/orchestrator.ts +7 -7
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +927 -66
- package/src/lib/server/provider-health.ts +38 -6
- package/src/lib/server/queue.ts +13 -28
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -82
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +366 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +114 -10
- package/src/lib/server/session-tools/context.ts +21 -5
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +74 -28
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +497 -24
- package/src/lib/server/session-tools/discovery.ts +24 -6
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +320 -0
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +241 -25
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +380 -0
- package/src/lib/server/session-tools/index.ts +130 -50
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +172 -3
- package/src/lib/server/session-tools/monitor.ts +151 -8
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +148 -7
- package/src/lib/server/session-tools/plugin-creator.ts +89 -26
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +301 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +24 -12
- package/src/lib/server/session-tools/session-info.ts +43 -7
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +194 -28
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +42 -12
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +926 -91
- package/src/lib/server/storage.ts +255 -16
- package/src/lib/server/stream-agent-chat.ts +116 -268
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -10
- package/src/lib/server/tool-aliases.ts +66 -18
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +38 -27
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +10 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +5 -11
- package/src/stores/use-chat-store.ts +38 -9
- package/src/types/index.ts +352 -47
- package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
- package/src/components/sessions/new-session-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -24
- package/src/lib/server/session-run-manager.test.ts +0 -23
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
package/src/lib/server/cost.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Agent, UsageRecord } from '@/types'
|
|
1
|
+
import type { Agent, UsageRecord, PluginDefinitionCost } from '@/types'
|
|
2
|
+
import type { StructuredToolInterface } from '@langchain/core/tools'
|
|
2
3
|
import { loadSessions, loadUsage } from './storage'
|
|
3
4
|
|
|
4
5
|
// Model cost table: [inputCostPer1M, outputCostPer1M] in USD
|
|
@@ -65,6 +66,38 @@ export function getModelCosts(): Record<string, [number, number]> {
|
|
|
65
66
|
return { ...MODEL_COSTS }
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Estimate the number of tokens a tool definition occupies in the LLM context.
|
|
71
|
+
* Uses ~4 chars per token as a rough approximation.
|
|
72
|
+
*/
|
|
73
|
+
export function estimateToolDefinitionTokens(t: StructuredToolInterface): number {
|
|
74
|
+
let chars = (t.name || '').length + (t.description || '').length
|
|
75
|
+
try {
|
|
76
|
+
const schema = typeof t.schema === 'object' ? JSON.stringify(t.schema) : ''
|
|
77
|
+
chars += schema.length
|
|
78
|
+
} catch { /* ignore */ }
|
|
79
|
+
return Math.ceil(chars / 4)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build per-plugin definition cost estimates from a set of tools and their plugin mapping.
|
|
84
|
+
*/
|
|
85
|
+
export function buildPluginDefinitionCosts(
|
|
86
|
+
tools: StructuredToolInterface[],
|
|
87
|
+
toolToPluginMap: Record<string, string>,
|
|
88
|
+
): PluginDefinitionCost[] {
|
|
89
|
+
const totals = new Map<string, number>()
|
|
90
|
+
for (const t of tools) {
|
|
91
|
+
const pluginId = toolToPluginMap[t.name] || '_unknown'
|
|
92
|
+
const tokens = estimateToolDefinitionTokens(t)
|
|
93
|
+
totals.set(pluginId, (totals.get(pluginId) || 0) + tokens)
|
|
94
|
+
}
|
|
95
|
+
return Array.from(totals.entries()).map(([pluginId, estimatedTokens]) => ({
|
|
96
|
+
pluginId,
|
|
97
|
+
estimatedTokens,
|
|
98
|
+
}))
|
|
99
|
+
}
|
|
100
|
+
|
|
68
101
|
export interface AgentSpendWindows {
|
|
69
102
|
hourly: number
|
|
70
103
|
daily: number
|
|
@@ -22,6 +22,12 @@ import type { WebhookRetryEntry } from '@/types'
|
|
|
22
22
|
import { createNotification } from '@/lib/server/create-notification'
|
|
23
23
|
import { pingProvider, OPENAI_COMPATIBLE_DEFAULTS } from '@/lib/server/provider-health'
|
|
24
24
|
import { runIntegrityMonitor } from '@/lib/server/integrity-monitor'
|
|
25
|
+
import { recoverStaleDelegationJobs } from './delegation-jobs'
|
|
26
|
+
import {
|
|
27
|
+
listPendingApprovalsNeedingConnectorNotification,
|
|
28
|
+
markApprovalConnectorNotificationAttempt,
|
|
29
|
+
markApprovalConnectorNotificationSent,
|
|
30
|
+
} from './approvals'
|
|
25
31
|
|
|
26
32
|
const QUEUE_CHECK_INTERVAL = 30_000 // 30 seconds
|
|
27
33
|
const BROWSER_SWEEP_INTERVAL = 60_000 // 60 seconds
|
|
@@ -161,6 +167,7 @@ export function startDaemon(options?: { source?: string; manualStart?: boolean }
|
|
|
161
167
|
try {
|
|
162
168
|
validateCompletedTasksQueue()
|
|
163
169
|
cleanupFinishedTaskSessions()
|
|
170
|
+
recoverStaleDelegationJobs()
|
|
164
171
|
startScheduler()
|
|
165
172
|
startQueueProcessor()
|
|
166
173
|
startBrowserSweep()
|
|
@@ -413,14 +420,14 @@ async function processWebhookRetries() {
|
|
|
413
420
|
claudeSessionId: null,
|
|
414
421
|
codexThreadId: null,
|
|
415
422
|
opencodeSessionId: null,
|
|
416
|
-
delegateResumeIds: { claudeCode: null, codex: null, opencode: null },
|
|
423
|
+
delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
|
|
417
424
|
messages: [],
|
|
418
425
|
createdAt: ts,
|
|
419
426
|
lastActiveAt: ts,
|
|
420
|
-
sessionType: '
|
|
427
|
+
sessionType: 'human',
|
|
421
428
|
agentId: agent.id,
|
|
422
429
|
parentSessionId: null,
|
|
423
|
-
|
|
430
|
+
plugins: agent.plugins || agent.tools || [],
|
|
424
431
|
heartbeatEnabled: (agent.heartbeatEnabled as boolean | undefined) ?? true,
|
|
425
432
|
heartbeatIntervalSec: (agent.heartbeatIntervalSec as number | null | undefined) ?? null,
|
|
426
433
|
}
|
|
@@ -664,6 +671,51 @@ async function runOpenClawGatewayHealthChecks() {
|
|
|
664
671
|
}
|
|
665
672
|
}
|
|
666
673
|
|
|
674
|
+
async function runPendingApprovalConnectorNotifications(now: number) {
|
|
675
|
+
const running = listRunningConnectors()
|
|
676
|
+
const pending = listPendingApprovalsNeedingConnectorNotification({
|
|
677
|
+
now,
|
|
678
|
+
runningConnectors: running,
|
|
679
|
+
})
|
|
680
|
+
if (!pending.length) return
|
|
681
|
+
|
|
682
|
+
for (const reminder of pending) {
|
|
683
|
+
try {
|
|
684
|
+
const result = await sendConnectorMessage({
|
|
685
|
+
connectorId: reminder.connectorId,
|
|
686
|
+
channelId: reminder.channelId,
|
|
687
|
+
text: reminder.text,
|
|
688
|
+
threadId: reminder.threadId || undefined,
|
|
689
|
+
})
|
|
690
|
+
markApprovalConnectorNotificationSent(reminder.approvalId, {
|
|
691
|
+
at: now,
|
|
692
|
+
connectorId: result.connectorId,
|
|
693
|
+
channelId: result.channelId,
|
|
694
|
+
threadId: reminder.threadId || null,
|
|
695
|
+
messageId: result.messageId || null,
|
|
696
|
+
})
|
|
697
|
+
createNotification({
|
|
698
|
+
type: 'info',
|
|
699
|
+
title: 'Approval reminder sent',
|
|
700
|
+
message: 'A pending approval reminder was delivered over an active connector.',
|
|
701
|
+
dedupKey: `approval-connector-reminder:${reminder.approvalId}`,
|
|
702
|
+
entityType: 'approval',
|
|
703
|
+
entityId: reminder.approvalId,
|
|
704
|
+
})
|
|
705
|
+
} catch (err: unknown) {
|
|
706
|
+
const errorMsg = err instanceof Error ? err.message : String(err)
|
|
707
|
+
markApprovalConnectorNotificationAttempt(reminder.approvalId, {
|
|
708
|
+
at: now,
|
|
709
|
+
connectorId: reminder.connectorId,
|
|
710
|
+
channelId: reminder.channelId,
|
|
711
|
+
threadId: reminder.threadId || null,
|
|
712
|
+
lastError: errorMsg,
|
|
713
|
+
})
|
|
714
|
+
console.warn(`[daemon] Approval connector reminder failed for ${reminder.approvalId}: ${errorMsg}`)
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
667
719
|
async function runHealthChecks() {
|
|
668
720
|
// Continuously keep the completed queue honest.
|
|
669
721
|
validateCompletedTasksQueue()
|
|
@@ -739,6 +791,12 @@ async function runHealthChecks() {
|
|
|
739
791
|
console.error('[daemon] OpenClaw gateway health check failed:', err instanceof Error ? err.message : String(err))
|
|
740
792
|
}
|
|
741
793
|
|
|
794
|
+
try {
|
|
795
|
+
await runPendingApprovalConnectorNotifications(now)
|
|
796
|
+
} catch (err: unknown) {
|
|
797
|
+
console.error('[daemon] Approval connector reminder check failed:', err instanceof Error ? err.message : String(err))
|
|
798
|
+
}
|
|
799
|
+
|
|
742
800
|
// Integrity drift monitoring for identity/config/plugin files.
|
|
743
801
|
try {
|
|
744
802
|
const integrity = runIntegrityMonitor(loadSettings())
|
|
@@ -19,3 +19,16 @@ function resolveWorkspaceDir(): string {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export const WORKSPACE_DIR = resolveWorkspaceDir()
|
|
22
|
+
|
|
23
|
+
function resolveBrowserProfilesDir(): string {
|
|
24
|
+
if (process.env.BROWSER_PROFILES_DIR) return process.env.BROWSER_PROFILES_DIR
|
|
25
|
+
const external = path.join(os.homedir(), '.swarmclaw', 'browser-profiles')
|
|
26
|
+
try {
|
|
27
|
+
fs.mkdirSync(external, { recursive: true })
|
|
28
|
+
return external
|
|
29
|
+
} catch {
|
|
30
|
+
return path.join(DATA_DIR, 'browser-profiles')
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const BROWSER_PROFILES_DIR = resolveBrowserProfilesDir()
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { after, before, describe, it } from 'node:test'
|
|
6
|
+
|
|
7
|
+
const originalEnv = {
|
|
8
|
+
DATA_DIR: process.env.DATA_DIR,
|
|
9
|
+
WORKSPACE_DIR: process.env.WORKSPACE_DIR,
|
|
10
|
+
SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let tempDir = ''
|
|
14
|
+
let delegationJobs: typeof import('./delegation-jobs')
|
|
15
|
+
|
|
16
|
+
before(async () => {
|
|
17
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-delegation-jobs-'))
|
|
18
|
+
process.env.DATA_DIR = path.join(tempDir, 'data')
|
|
19
|
+
process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
|
|
20
|
+
process.env.SWARMCLAW_BUILD_MODE = '1'
|
|
21
|
+
delegationJobs = await import('./delegation-jobs')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
after(() => {
|
|
25
|
+
if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
|
|
26
|
+
else process.env.DATA_DIR = originalEnv.DATA_DIR
|
|
27
|
+
if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
|
|
28
|
+
else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
|
|
29
|
+
if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
|
|
30
|
+
else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
|
|
31
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('delegation-jobs', () => {
|
|
35
|
+
it('tracks a queued job through running and completion', () => {
|
|
36
|
+
const job = delegationJobs.createDelegationJob({
|
|
37
|
+
kind: 'delegate',
|
|
38
|
+
parentSessionId: 'session-1',
|
|
39
|
+
backend: 'codex',
|
|
40
|
+
task: 'Refactor the module',
|
|
41
|
+
cwd: process.cwd(),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const started = delegationJobs.startDelegationJob(job.id, { backend: 'codex' })
|
|
45
|
+
const completed = delegationJobs.completeDelegationJob(job.id, 'done')
|
|
46
|
+
|
|
47
|
+
assert.equal(started?.status, 'running')
|
|
48
|
+
assert.equal(completed?.status, 'completed')
|
|
49
|
+
assert.equal(completed?.resultPreview, 'done')
|
|
50
|
+
assert.equal(delegationJobs.listDelegationJobs({ parentSessionId: 'session-1' }).length >= 1, true)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('keeps cancellation terminal even if late completions arrive', () => {
|
|
54
|
+
const job = delegationJobs.createDelegationJob({
|
|
55
|
+
kind: 'subagent',
|
|
56
|
+
parentSessionId: 'session-2',
|
|
57
|
+
agentId: 'agent-1',
|
|
58
|
+
task: 'Do the background work',
|
|
59
|
+
cwd: process.cwd(),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
let cancelled = false
|
|
63
|
+
delegationJobs.startDelegationJob(job.id, { agentId: 'agent-1' })
|
|
64
|
+
delegationJobs.registerDelegationRuntime(job.id, {
|
|
65
|
+
cancel: () => {
|
|
66
|
+
cancelled = true
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const stopped = delegationJobs.cancelDelegationJob(job.id)
|
|
71
|
+
const afterComplete = delegationJobs.completeDelegationJob(job.id, 'late success')
|
|
72
|
+
const afterFail = delegationJobs.failDelegationJob(job.id, 'late failure')
|
|
73
|
+
|
|
74
|
+
assert.equal(cancelled, true)
|
|
75
|
+
assert.equal(stopped?.status, 'cancelled')
|
|
76
|
+
assert.equal(afterComplete?.status, 'cancelled')
|
|
77
|
+
assert.equal(afterFail?.status, 'cancelled')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('recovers stale running jobs as failed', () => {
|
|
81
|
+
const stale = delegationJobs.createDelegationJob({
|
|
82
|
+
kind: 'delegate',
|
|
83
|
+
parentSessionId: 'session-3',
|
|
84
|
+
backend: 'claude',
|
|
85
|
+
task: 'Stale job',
|
|
86
|
+
cwd: process.cwd(),
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
delegationJobs.startDelegationJob(stale.id)
|
|
90
|
+
delegationJobs.updateDelegationJob(stale.id, {
|
|
91
|
+
startedAt: Date.now() - 60_000,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const recovered = delegationJobs.recoverStaleDelegationJobs(-1)
|
|
95
|
+
const latest = delegationJobs.getDelegationJob(stale.id)
|
|
96
|
+
|
|
97
|
+
assert.equal(recovered >= 1, true)
|
|
98
|
+
assert.equal(latest?.status, 'failed')
|
|
99
|
+
assert.match(String(latest?.error || ''), /interrupted/i)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('cancels all running jobs for a parent session', () => {
|
|
103
|
+
const jobA = delegationJobs.createDelegationJob({
|
|
104
|
+
kind: 'delegate',
|
|
105
|
+
parentSessionId: 'session-bulk',
|
|
106
|
+
backend: 'codex',
|
|
107
|
+
task: 'Task A',
|
|
108
|
+
cwd: process.cwd(),
|
|
109
|
+
})
|
|
110
|
+
const jobB = delegationJobs.createDelegationJob({
|
|
111
|
+
kind: 'subagent',
|
|
112
|
+
parentSessionId: 'session-bulk',
|
|
113
|
+
agentId: 'agent-2',
|
|
114
|
+
task: 'Task B',
|
|
115
|
+
cwd: process.cwd(),
|
|
116
|
+
})
|
|
117
|
+
const untouched = delegationJobs.createDelegationJob({
|
|
118
|
+
kind: 'delegate',
|
|
119
|
+
parentSessionId: 'other-session',
|
|
120
|
+
backend: 'claude',
|
|
121
|
+
task: 'Task C',
|
|
122
|
+
cwd: process.cwd(),
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
delegationJobs.startDelegationJob(jobA.id)
|
|
126
|
+
delegationJobs.startDelegationJob(jobB.id)
|
|
127
|
+
delegationJobs.startDelegationJob(untouched.id)
|
|
128
|
+
|
|
129
|
+
const cancelled = delegationJobs.cancelDelegationJobsForParentSession('session-bulk', 'Stopped by user')
|
|
130
|
+
|
|
131
|
+
assert.equal(cancelled, 2)
|
|
132
|
+
assert.equal(delegationJobs.getDelegationJob(jobA.id)?.status, 'cancelled')
|
|
133
|
+
assert.equal(delegationJobs.getDelegationJob(jobB.id)?.status, 'cancelled')
|
|
134
|
+
assert.equal(delegationJobs.getDelegationJob(untouched.id)?.status, 'running')
|
|
135
|
+
assert.equal(
|
|
136
|
+
delegationJobs.getDelegationJob(jobA.id)?.checkpoints?.some((entry) => entry.note === 'Stopped by user'),
|
|
137
|
+
true,
|
|
138
|
+
)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
|
+
import type { DelegationJobArtifact, DelegationJobCheckpoint, DelegationJobRecord, DelegationJobStatus } from '@/types'
|
|
3
|
+
import { loadDelegationJobs, upsertDelegationJob } from './storage'
|
|
4
|
+
import { notify } from './ws-hub'
|
|
5
|
+
|
|
6
|
+
interface DelegationRuntimeHandle {
|
|
7
|
+
cancel?: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const runtimeKey = '__swarmclaw_delegation_job_runtime__' as const
|
|
11
|
+
const runtimeScope = globalThis as typeof globalThis & {
|
|
12
|
+
[runtimeKey]?: Map<string, DelegationRuntimeHandle>
|
|
13
|
+
}
|
|
14
|
+
const runtimeHandles = runtimeScope[runtimeKey] ?? (runtimeScope[runtimeKey] = new Map())
|
|
15
|
+
|
|
16
|
+
function now() {
|
|
17
|
+
return Date.now()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isTerminalStatus(status: DelegationJobStatus | null | undefined): boolean {
|
|
21
|
+
return status === 'completed' || status === 'failed' || status === 'cancelled'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function notifyDelegationJobsChanged() {
|
|
25
|
+
notify('delegation_jobs')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CreateDelegationJobInput {
|
|
29
|
+
kind: DelegationJobRecord['kind']
|
|
30
|
+
task: string
|
|
31
|
+
backend?: DelegationJobRecord['backend']
|
|
32
|
+
parentSessionId?: string | null
|
|
33
|
+
childSessionId?: string | null
|
|
34
|
+
agentId?: string | null
|
|
35
|
+
agentName?: string | null
|
|
36
|
+
cwd?: string | null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createDelegationJob(input: CreateDelegationJobInput): DelegationJobRecord {
|
|
40
|
+
const createdAt = now()
|
|
41
|
+
const job: DelegationJobRecord = {
|
|
42
|
+
id: genId(10),
|
|
43
|
+
kind: input.kind,
|
|
44
|
+
status: 'queued',
|
|
45
|
+
backend: input.backend ?? null,
|
|
46
|
+
parentSessionId: input.parentSessionId ?? null,
|
|
47
|
+
childSessionId: input.childSessionId ?? null,
|
|
48
|
+
agentId: input.agentId ?? null,
|
|
49
|
+
agentName: input.agentName ?? null,
|
|
50
|
+
cwd: input.cwd ?? null,
|
|
51
|
+
task: input.task,
|
|
52
|
+
result: null,
|
|
53
|
+
resultPreview: null,
|
|
54
|
+
error: null,
|
|
55
|
+
checkpoints: [{
|
|
56
|
+
at: createdAt,
|
|
57
|
+
note: 'Job queued',
|
|
58
|
+
status: 'queued',
|
|
59
|
+
}],
|
|
60
|
+
artifacts: [],
|
|
61
|
+
resumeId: null,
|
|
62
|
+
resumeIds: {},
|
|
63
|
+
createdAt,
|
|
64
|
+
updatedAt: createdAt,
|
|
65
|
+
startedAt: null,
|
|
66
|
+
completedAt: null,
|
|
67
|
+
}
|
|
68
|
+
upsertDelegationJob(job.id, job)
|
|
69
|
+
notifyDelegationJobsChanged()
|
|
70
|
+
return job
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getDelegationJob(id: string): DelegationJobRecord | null {
|
|
74
|
+
const all = loadDelegationJobs()
|
|
75
|
+
const current = all[id]
|
|
76
|
+
if (!current || typeof current !== 'object') return null
|
|
77
|
+
return current as DelegationJobRecord
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function listDelegationJobs(filter?: {
|
|
81
|
+
parentSessionId?: string | null
|
|
82
|
+
status?: DelegationJobStatus | null
|
|
83
|
+
}): DelegationJobRecord[] {
|
|
84
|
+
return Object.values(loadDelegationJobs())
|
|
85
|
+
.filter((job): job is DelegationJobRecord => !!job && typeof job === 'object')
|
|
86
|
+
.filter((job) => !filter?.parentSessionId || job.parentSessionId === filter.parentSessionId)
|
|
87
|
+
.filter((job) => !filter?.status || job.status === filter.status)
|
|
88
|
+
.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function updateDelegationJob(
|
|
92
|
+
id: string,
|
|
93
|
+
patch: Partial<DelegationJobRecord>,
|
|
94
|
+
): DelegationJobRecord | null {
|
|
95
|
+
const current = getDelegationJob(id)
|
|
96
|
+
if (!current) return null
|
|
97
|
+
const next: DelegationJobRecord = {
|
|
98
|
+
...current,
|
|
99
|
+
...patch,
|
|
100
|
+
updatedAt: now(),
|
|
101
|
+
}
|
|
102
|
+
upsertDelegationJob(id, next)
|
|
103
|
+
notifyDelegationJobsChanged()
|
|
104
|
+
return next
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function appendDelegationCheckpoint(
|
|
108
|
+
id: string,
|
|
109
|
+
note: string,
|
|
110
|
+
status?: DelegationJobStatus,
|
|
111
|
+
): DelegationJobRecord | null {
|
|
112
|
+
const current = getDelegationJob(id)
|
|
113
|
+
if (!current) return null
|
|
114
|
+
if (isTerminalStatus(current.status) && status && status !== current.status) {
|
|
115
|
+
return current
|
|
116
|
+
}
|
|
117
|
+
const checkpoints = [...(current.checkpoints || []), { at: now(), note, status }]
|
|
118
|
+
return updateDelegationJob(id, {
|
|
119
|
+
status: isTerminalStatus(current.status) ? current.status : (status ?? current.status),
|
|
120
|
+
checkpoints: checkpoints.slice(-24),
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function startDelegationJob(id: string, patch?: Partial<DelegationJobRecord>): DelegationJobRecord | null {
|
|
125
|
+
const current = getDelegationJob(id)
|
|
126
|
+
if (!current) return null
|
|
127
|
+
if (isTerminalStatus(current.status)) return current
|
|
128
|
+
return updateDelegationJob(id, {
|
|
129
|
+
...patch,
|
|
130
|
+
status: 'running',
|
|
131
|
+
startedAt: now(),
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function completeDelegationJob(
|
|
136
|
+
id: string,
|
|
137
|
+
result: string,
|
|
138
|
+
patch?: Partial<DelegationJobRecord>,
|
|
139
|
+
): DelegationJobRecord | null {
|
|
140
|
+
runtimeHandles.delete(id)
|
|
141
|
+
const current = getDelegationJob(id)
|
|
142
|
+
if (!current) return null
|
|
143
|
+
if (isTerminalStatus(current.status)) return current
|
|
144
|
+
return updateDelegationJob(id, {
|
|
145
|
+
...patch,
|
|
146
|
+
status: 'completed',
|
|
147
|
+
result,
|
|
148
|
+
resultPreview: result.slice(0, 1000),
|
|
149
|
+
error: null,
|
|
150
|
+
completedAt: now(),
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function failDelegationJob(id: string, error: string, patch?: Partial<DelegationJobRecord>): DelegationJobRecord | null {
|
|
155
|
+
runtimeHandles.delete(id)
|
|
156
|
+
const current = getDelegationJob(id)
|
|
157
|
+
if (!current) return null
|
|
158
|
+
if (isTerminalStatus(current.status)) return current
|
|
159
|
+
return updateDelegationJob(id, {
|
|
160
|
+
...patch,
|
|
161
|
+
status: 'failed',
|
|
162
|
+
error,
|
|
163
|
+
completedAt: now(),
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function cancelDelegationJob(id: string): DelegationJobRecord | null {
|
|
168
|
+
const current = getDelegationJob(id)
|
|
169
|
+
if (!current) return null
|
|
170
|
+
if (isTerminalStatus(current.status)) return current
|
|
171
|
+
const runtime = runtimeHandles.get(id)
|
|
172
|
+
try {
|
|
173
|
+
runtime?.cancel?.()
|
|
174
|
+
} catch {
|
|
175
|
+
// best-effort cancel
|
|
176
|
+
}
|
|
177
|
+
runtimeHandles.delete(id)
|
|
178
|
+
const checkpoint: DelegationJobCheckpoint = {
|
|
179
|
+
at: now(),
|
|
180
|
+
note: 'Job cancelled',
|
|
181
|
+
status: 'cancelled',
|
|
182
|
+
}
|
|
183
|
+
return updateDelegationJob(id, {
|
|
184
|
+
status: 'cancelled',
|
|
185
|
+
completedAt: now(),
|
|
186
|
+
error: null,
|
|
187
|
+
checkpoints: [
|
|
188
|
+
...(current.checkpoints || []),
|
|
189
|
+
checkpoint,
|
|
190
|
+
].slice(-24),
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function cancelDelegationJobsForParentSession(
|
|
195
|
+
parentSessionId: string,
|
|
196
|
+
note = 'Parent session cancelled',
|
|
197
|
+
): number {
|
|
198
|
+
if (!parentSessionId) return 0
|
|
199
|
+
const jobs = listDelegationJobs({ parentSessionId })
|
|
200
|
+
.filter((job) => job.status === 'queued' || job.status === 'running')
|
|
201
|
+
let cancelled = 0
|
|
202
|
+
for (const job of jobs) {
|
|
203
|
+
const next = cancelDelegationJob(job.id)
|
|
204
|
+
if (!next || next.status !== 'cancelled') continue
|
|
205
|
+
cancelled += 1
|
|
206
|
+
const checkpoints = Array.isArray(next.checkpoints) ? next.checkpoints : []
|
|
207
|
+
const last = checkpoints[checkpoints.length - 1]
|
|
208
|
+
if (!last || last.note !== note) {
|
|
209
|
+
const checkpoint: DelegationJobCheckpoint = {
|
|
210
|
+
at: now(),
|
|
211
|
+
note,
|
|
212
|
+
status: 'cancelled',
|
|
213
|
+
}
|
|
214
|
+
updateDelegationJob(job.id, {
|
|
215
|
+
checkpoints: [
|
|
216
|
+
...checkpoints,
|
|
217
|
+
checkpoint,
|
|
218
|
+
].slice(-24),
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return cancelled
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function registerDelegationRuntime(id: string, handle: DelegationRuntimeHandle) {
|
|
226
|
+
runtimeHandles.set(id, handle)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function appendDelegationArtifacts(id: string, artifacts: DelegationJobArtifact[]): DelegationJobRecord | null {
|
|
230
|
+
const current = getDelegationJob(id)
|
|
231
|
+
if (!current) return null
|
|
232
|
+
return updateDelegationJob(id, {
|
|
233
|
+
artifacts: [...(current.artifacts || []), ...artifacts].slice(-24),
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function recoverStaleDelegationJobs(maxAgeMs = 15 * 60_000): number {
|
|
238
|
+
const threshold = now() - maxAgeMs
|
|
239
|
+
const stale = listDelegationJobs().filter((job) =>
|
|
240
|
+
(job.status === 'queued' || job.status === 'running')
|
|
241
|
+
&& !runtimeHandles.has(job.id)
|
|
242
|
+
&& (job.updatedAt || job.createdAt) < threshold,
|
|
243
|
+
)
|
|
244
|
+
for (const job of stale) {
|
|
245
|
+
failDelegationJob(job.id, 'Delegation job was interrupted before completion.')
|
|
246
|
+
}
|
|
247
|
+
return stale.length
|
|
248
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { describe, it } from 'node:test'
|
|
6
|
+
import { extractDocumentArtifact, loadTabularFile, normalizeInlineRows, writeStructuredTable } from './document-utils'
|
|
7
|
+
|
|
8
|
+
describe('document-utils', () => {
|
|
9
|
+
it('extracts structured tables from JSON arrays', async () => {
|
|
10
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-doc-utils-'))
|
|
11
|
+
try {
|
|
12
|
+
const jsonPath = path.join(tempDir, 'people.json')
|
|
13
|
+
fs.writeFileSync(jsonPath, JSON.stringify([
|
|
14
|
+
{ name: 'Ada', score: 10 },
|
|
15
|
+
{ name: 'Grace', score: 9 },
|
|
16
|
+
], null, 2))
|
|
17
|
+
|
|
18
|
+
const artifact = await extractDocumentArtifact(jsonPath)
|
|
19
|
+
assert.equal(artifact.method, 'json')
|
|
20
|
+
assert.equal(artifact.tables.length, 1)
|
|
21
|
+
assert.deepEqual(artifact.tables[0].headers, ['name', 'score'])
|
|
22
|
+
assert.equal(artifact.tables[0].rowCount, 2)
|
|
23
|
+
} finally {
|
|
24
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('normalizes inline rows and round-trips CSV output', async () => {
|
|
29
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-doc-utils-'))
|
|
30
|
+
try {
|
|
31
|
+
const table = normalizeInlineRows([
|
|
32
|
+
{ city: 'London', population: 9 },
|
|
33
|
+
{ city: 'Paris', population: 2 },
|
|
34
|
+
])
|
|
35
|
+
const csvPath = path.join(tempDir, 'cities.csv')
|
|
36
|
+
const written = await writeStructuredTable(csvPath, table)
|
|
37
|
+
const loaded = await loadTabularFile(csvPath)
|
|
38
|
+
|
|
39
|
+
assert.equal(written.format, 'csv')
|
|
40
|
+
assert.deepEqual(loaded.headers, ['city', 'population'])
|
|
41
|
+
assert.equal(loaded.rowCount, 2)
|
|
42
|
+
assert.equal(String(loaded.rows[0].city), 'London')
|
|
43
|
+
} finally {
|
|
44
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
})
|