@swarmclawai/swarmclaw 0.7.2 → 0.7.4
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 +116 -50
- package/bin/package-manager.js +157 -0
- package/bin/package-manager.test.js +90 -0
- package/bin/server-cmd.js +38 -7
- package/bin/swarmclaw.js +54 -4
- package/bin/update-cmd.js +48 -10
- package/bin/update-cmd.test.js +55 -0
- package/package.json +8 -3
- package/scripts/postinstall.mjs +26 -0
- package/src/app/api/agents/[id]/route.ts +43 -0
- package/src/app/api/agents/[id]/thread/route.ts +39 -8
- package/src/app/api/agents/route.ts +35 -2
- package/src/app/api/auth/route.ts +77 -8
- package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
- package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/route.ts +6 -0
- package/src/app/api/chats/[id]/browser/route.ts +5 -1
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +30 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +23 -1
- 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/external-agents/[id]/heartbeat/route.ts +33 -0
- package/src/app/api/external-agents/[id]/route.ts +31 -0
- package/src/app/api/external-agents/register/route.ts +3 -0
- package/src/app/api/external-agents/route.ts +66 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/gateways/[id]/health/route.ts +28 -0
- package/src/app/api/gateways/[id]/route.ts +79 -0
- package/src/app/api/gateways/route.ts +57 -0
- 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/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +12 -4
- 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 +3 -26
- package/src/app/api/plugins/settings/route.ts +17 -12
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
- package/src/app/api/schedules/[id]/route.ts +38 -9
- package/src/app/api/schedules/route.ts +51 -28
- package/src/app/api/settings/route.ts +55 -17
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +16 -6
- package/src/app/api/tasks/bulk/route.ts +3 -3
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +135 -17
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +38 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +21 -12
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-card.tsx +15 -12
- package/src/components/agents/agent-chat-list.tsx +101 -1
- package/src/components/agents/agent-list.tsx +46 -9
- package/src/components/agents/agent-sheet.tsx +456 -23
- package/src/components/agents/inspector-panel.tsx +110 -49
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +70 -27
- package/src/components/chat/chat-card.tsx +6 -21
- package/src/components/chat/chat-header.tsx +263 -366
- package/src/components/chat/chat-list.tsx +62 -26
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +145 -19
- package/src/components/chatrooms/chatroom-input.tsx +96 -33
- package/src/components/chatrooms/chatroom-list.tsx +141 -72
- package/src/components/chatrooms/chatroom-message.tsx +7 -6
- package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
- package/src/components/chatrooms/chatroom-view.tsx +422 -209
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +217 -0
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +385 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/memory/memory-browser.tsx +71 -6
- package/src/components/memory/memory-card.tsx +18 -0
- package/src/components/memory/memory-detail.tsx +58 -31
- package/src/components/memory/memory-sheet.tsx +32 -4
- package/src/components/plugins/plugin-list.tsx +15 -3
- package/src/components/plugins/plugin-sheet.tsx +118 -9
- package/src/components/projects/project-detail.tsx +189 -1
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/command-palette.tsx +111 -24
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- 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 +88 -6
- package/src/components/shared/settings/section-orchestrator.tsx +6 -3
- 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 +248 -47
- package/src/components/tasks/approvals-panel.tsx +211 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/usage/metrics-dashboard.tsx +74 -1
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +7 -7
- 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/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/provider-model-discovery-client.ts +29 -0
- package/src/lib/providers/index.ts +12 -5
- package/src/lib/runtime-loop.ts +105 -3
- package/src/lib/safe-storage.ts +6 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/agent-runtime-config.test.ts +141 -0
- package/src/lib/server/agent-runtime-config.ts +277 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +264 -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 +44 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
- package/src/lib/server/chat-execution.ts +402 -125
- 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 +74 -2
- package/src/lib/server/chatroom-helpers.ts +144 -11
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- 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 +994 -130
- 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 +189 -10
- 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/daemon-state.ts +62 -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/eval/agent-regression.test.ts +47 -0
- package/src/lib/server/eval/agent-regression.ts +1742 -0
- package/src/lib/server/eval/runner.ts +11 -1
- package/src/lib/server/eval/store.ts +2 -1
- package/src/lib/server/heartbeat-service.ts +23 -43
- 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 +31 -964
- 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 +6 -5
- package/src/lib/server/openclaw-gateway.ts +123 -36
- 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 +18 -8
- package/src/lib/server/orchestrator.ts +5 -4
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +215 -0
- package/src/lib/server/plugins.ts +832 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +4 -21
- package/src/lib/server/runtime-settings.test.ts +119 -0
- package/src/lib/server/runtime-settings.ts +12 -92
- package/src/lib/server/schedule-normalization.ts +187 -0
- 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 -80
- package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
- package/src/lib/server/session-tools/calendar.ts +2 -12
- package/src/lib/server/session-tools/connector.ts +109 -8
- package/src/lib/server/session-tools/context.ts +14 -2
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +96 -34
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +406 -20
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +40 -12
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/email.ts +1 -3
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +243 -24
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +1 -3
- package/src/lib/server/session-tools/index.ts +87 -2
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +162 -12
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +142 -4
- package/src/lib/server/session-tools/plugin-creator.ts +95 -25
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +1 -3
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +58 -4
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +195 -27
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +13 -10
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +947 -108
- package/src/lib/server/storage.ts +255 -10
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +185 -25
- 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 -11
- package/src/lib/server/tool-aliases.ts +80 -12
- package/src/lib/server/tool-capability-policy.ts +7 -1
- 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/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +62 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +43 -7
- package/src/stores/use-chat-store.ts +31 -2
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +470 -44
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
- package/src/components/chat/new-chat-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -17
- package/src/lib/server/session-run-manager.test.ts +0 -26
|
@@ -16,12 +16,19 @@ import { startHeartbeatService, stopHeartbeatService, getHeartbeatServiceStatus
|
|
|
16
16
|
import { hasOpenClawAgents, ensureGatewayConnected, disconnectGateway, getGateway } from './openclaw-gateway'
|
|
17
17
|
import { enqueueSessionRun } from './session-run-manager'
|
|
18
18
|
import { WORKSPACE_DIR } from './data-dir'
|
|
19
|
+
import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/heartbeat-defaults'
|
|
19
20
|
import { genId } from '@/lib/id'
|
|
20
21
|
import path from 'node:path'
|
|
21
22
|
import type { WebhookRetryEntry } from '@/types'
|
|
22
23
|
import { createNotification } from '@/lib/server/create-notification'
|
|
23
24
|
import { pingProvider, OPENAI_COMPATIBLE_DEFAULTS } from '@/lib/server/provider-health'
|
|
24
25
|
import { runIntegrityMonitor } from '@/lib/server/integrity-monitor'
|
|
26
|
+
import { recoverStaleDelegationJobs } from './delegation-jobs'
|
|
27
|
+
import {
|
|
28
|
+
listPendingApprovalsNeedingConnectorNotification,
|
|
29
|
+
markApprovalConnectorNotificationAttempt,
|
|
30
|
+
markApprovalConnectorNotificationSent,
|
|
31
|
+
} from './approvals'
|
|
25
32
|
|
|
26
33
|
const QUEUE_CHECK_INTERVAL = 30_000 // 30 seconds
|
|
27
34
|
const BROWSER_SWEEP_INTERVAL = 60_000 // 60 seconds
|
|
@@ -51,7 +58,7 @@ function daemonAutostartEnvEnabled(): boolean {
|
|
|
51
58
|
return parseBoolish(process.env.SWARMCLAW_DAEMON_AUTOSTART, true)
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
function parseHeartbeatIntervalSec(value: unknown, fallback =
|
|
61
|
+
function parseHeartbeatIntervalSec(value: unknown, fallback = DEFAULT_HEARTBEAT_INTERVAL_SEC): number {
|
|
55
62
|
const parsed = typeof value === 'number'
|
|
56
63
|
? value
|
|
57
64
|
: typeof value === 'string'
|
|
@@ -161,6 +168,7 @@ export function startDaemon(options?: { source?: string; manualStart?: boolean }
|
|
|
161
168
|
try {
|
|
162
169
|
validateCompletedTasksQueue()
|
|
163
170
|
cleanupFinishedTaskSessions()
|
|
171
|
+
recoverStaleDelegationJobs()
|
|
164
172
|
startScheduler()
|
|
165
173
|
startQueueProcessor()
|
|
166
174
|
startBrowserSweep()
|
|
@@ -417,7 +425,7 @@ async function processWebhookRetries() {
|
|
|
417
425
|
messages: [],
|
|
418
426
|
createdAt: ts,
|
|
419
427
|
lastActiveAt: ts,
|
|
420
|
-
sessionType: '
|
|
428
|
+
sessionType: 'human',
|
|
421
429
|
agentId: agent.id,
|
|
422
430
|
parentSessionId: null,
|
|
423
431
|
plugins: agent.plugins || agent.tools || [],
|
|
@@ -664,6 +672,51 @@ async function runOpenClawGatewayHealthChecks() {
|
|
|
664
672
|
}
|
|
665
673
|
}
|
|
666
674
|
|
|
675
|
+
async function runPendingApprovalConnectorNotifications(now: number) {
|
|
676
|
+
const running = listRunningConnectors()
|
|
677
|
+
const pending = listPendingApprovalsNeedingConnectorNotification({
|
|
678
|
+
now,
|
|
679
|
+
runningConnectors: running,
|
|
680
|
+
})
|
|
681
|
+
if (!pending.length) return
|
|
682
|
+
|
|
683
|
+
for (const reminder of pending) {
|
|
684
|
+
try {
|
|
685
|
+
const result = await sendConnectorMessage({
|
|
686
|
+
connectorId: reminder.connectorId,
|
|
687
|
+
channelId: reminder.channelId,
|
|
688
|
+
text: reminder.text,
|
|
689
|
+
threadId: reminder.threadId || undefined,
|
|
690
|
+
})
|
|
691
|
+
markApprovalConnectorNotificationSent(reminder.approvalId, {
|
|
692
|
+
at: now,
|
|
693
|
+
connectorId: result.connectorId,
|
|
694
|
+
channelId: result.channelId,
|
|
695
|
+
threadId: reminder.threadId || null,
|
|
696
|
+
messageId: result.messageId || null,
|
|
697
|
+
})
|
|
698
|
+
createNotification({
|
|
699
|
+
type: 'info',
|
|
700
|
+
title: 'Approval reminder sent',
|
|
701
|
+
message: 'A pending approval reminder was delivered over an active connector.',
|
|
702
|
+
dedupKey: `approval-connector-reminder:${reminder.approvalId}`,
|
|
703
|
+
entityType: 'approval',
|
|
704
|
+
entityId: reminder.approvalId,
|
|
705
|
+
})
|
|
706
|
+
} catch (err: unknown) {
|
|
707
|
+
const errorMsg = err instanceof Error ? err.message : String(err)
|
|
708
|
+
markApprovalConnectorNotificationAttempt(reminder.approvalId, {
|
|
709
|
+
at: now,
|
|
710
|
+
connectorId: reminder.connectorId,
|
|
711
|
+
channelId: reminder.channelId,
|
|
712
|
+
threadId: reminder.threadId || null,
|
|
713
|
+
lastError: errorMsg,
|
|
714
|
+
})
|
|
715
|
+
console.warn(`[daemon] Approval connector reminder failed for ${reminder.approvalId}: ${errorMsg}`)
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
667
720
|
async function runHealthChecks() {
|
|
668
721
|
// Continuously keep the completed queue honest.
|
|
669
722
|
validateCompletedTasksQueue()
|
|
@@ -683,7 +736,7 @@ async function runHealthChecks() {
|
|
|
683
736
|
|
|
684
737
|
const sessionId = session.id
|
|
685
738
|
const sessionLabel = String(session.name || sessionId)
|
|
686
|
-
const intervalSec = parseHeartbeatIntervalSec(session.heartbeatIntervalSec,
|
|
739
|
+
const intervalSec = parseHeartbeatIntervalSec(session.heartbeatIntervalSec, DEFAULT_HEARTBEAT_INTERVAL_SEC)
|
|
687
740
|
if (intervalSec <= 0) continue
|
|
688
741
|
const staleAfter = Math.max(intervalSec * STALE_MULTIPLIER * 1000, STALE_MIN_MS)
|
|
689
742
|
const lastActive = typeof session.lastActiveAt === 'number' ? session.lastActiveAt : 0
|
|
@@ -739,6 +792,12 @@ async function runHealthChecks() {
|
|
|
739
792
|
console.error('[daemon] OpenClaw gateway health check failed:', err instanceof Error ? err.message : String(err))
|
|
740
793
|
}
|
|
741
794
|
|
|
795
|
+
try {
|
|
796
|
+
await runPendingApprovalConnectorNotifications(now)
|
|
797
|
+
} catch (err: unknown) {
|
|
798
|
+
console.error('[daemon] Approval connector reminder check failed:', err instanceof Error ? err.message : String(err))
|
|
799
|
+
}
|
|
800
|
+
|
|
742
801
|
// Integrity drift monitoring for identity/config/plugin files.
|
|
743
802
|
try {
|
|
744
803
|
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
|
+
})
|