@swarmclawai/swarmclaw 0.4.0 → 0.5.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 +21 -4
- package/bin/server-cmd.js +28 -19
- package/next.config.ts +13 -0
- package/package.json +3 -1
- package/src/app/api/agents/[id]/route.ts +39 -22
- package/src/app/api/agents/[id]/thread/route.ts +2 -2
- package/src/app/api/agents/route.ts +3 -2
- package/src/app/api/agents/trash/route.ts +44 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +17 -7
- package/src/app/api/connectors/[id]/webhook/route.ts +103 -0
- package/src/app/api/connectors/route.ts +6 -3
- package/src/app/api/credentials/[id]/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -2
- package/src/app/api/documents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +8 -0
- package/src/app/api/knowledge/[id]/route.ts +5 -4
- package/src/app/api/knowledge/upload/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/route.ts +11 -14
- package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
- package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
- package/src/app/api/mcp-servers/route.ts +2 -2
- package/src/app/api/memory/[id]/route.ts +9 -8
- package/src/app/api/memory/route.ts +2 -2
- package/src/app/api/memory-images/[filename]/route.ts +2 -1
- package/src/app/api/openclaw/agent-files/route.ts +57 -0
- package/src/app/api/openclaw/approvals/route.ts +46 -0
- package/src/app/api/openclaw/config-sync/route.ts +33 -0
- package/src/app/api/openclaw/cron/route.ts +52 -0
- package/src/app/api/openclaw/directory/route.ts +27 -0
- package/src/app/api/openclaw/discover/route.ts +62 -0
- package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
- package/src/app/api/openclaw/exec-config/route.ts +41 -0
- package/src/app/api/openclaw/gateway/route.ts +72 -0
- package/src/app/api/openclaw/history/route.ts +109 -0
- package/src/app/api/openclaw/media/route.ts +53 -0
- package/src/app/api/openclaw/models/route.ts +12 -0
- package/src/app/api/openclaw/permissions/route.ts +39 -0
- package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
- package/src/app/api/openclaw/skills/install/route.ts +32 -0
- package/src/app/api/openclaw/skills/remove/route.ts +24 -0
- package/src/app/api/openclaw/skills/route.ts +82 -0
- package/src/app/api/openclaw/sync/route.ts +31 -0
- package/src/app/api/orchestrator/run/route.ts +2 -2
- package/src/app/api/projects/[id]/route.ts +55 -0
- package/src/app/api/projects/route.ts +27 -0
- package/src/app/api/providers/[id]/models/route.ts +2 -1
- package/src/app/api/providers/[id]/route.ts +13 -15
- package/src/app/api/providers/route.ts +2 -2
- package/src/app/api/schedules/[id]/route.ts +16 -18
- package/src/app/api/schedules/[id]/run/route.ts +4 -3
- package/src/app/api/schedules/route.ts +2 -2
- package/src/app/api/secrets/[id]/route.ts +16 -17
- package/src/app/api/secrets/route.ts +2 -2
- package/src/app/api/sessions/[id]/clear/route.ts +2 -1
- package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
- package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
- package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
- package/src/app/api/sessions/[id]/fork/route.ts +44 -0
- package/src/app/api/sessions/[id]/messages/route.ts +20 -2
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +14 -4
- package/src/app/api/sessions/route.ts +8 -4
- package/src/app/api/skills/[id]/route.ts +23 -21
- package/src/app/api/skills/import/route.ts +2 -2
- package/src/app/api/skills/route.ts +2 -2
- package/src/app/api/tasks/[id]/approve/route.ts +2 -1
- package/src/app/api/tasks/[id]/route.ts +6 -5
- package/src/app/api/tasks/route.ts +2 -2
- package/src/app/api/tts/stream/route.ts +48 -0
- package/src/app/api/upload/route.ts +2 -2
- package/src/app/api/uploads/[filename]/route.ts +4 -1
- package/src/app/api/webhooks/[id]/route.ts +29 -31
- package/src/app/api/webhooks/route.ts +2 -2
- package/src/app/globals.css +14 -0
- package/src/app/layout.tsx +5 -20
- package/src/app/page.tsx +3 -24
- package/src/cli/index.js +60 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/spec.js +42 -0
- package/src/components/agents/agent-avatar.tsx +45 -0
- package/src/components/agents/agent-card.tsx +19 -5
- package/src/components/agents/agent-chat-list.tsx +31 -24
- package/src/components/agents/agent-files-editor.tsx +185 -0
- package/src/components/agents/agent-list.tsx +84 -3
- package/src/components/agents/agent-sheet.tsx +147 -14
- package/src/components/agents/cron-job-form.tsx +137 -0
- package/src/components/agents/exec-config-panel.tsx +147 -0
- package/src/components/agents/inspector-panel.tsx +310 -0
- package/src/components/agents/openclaw-skills-panel.tsx +230 -0
- package/src/components/agents/permission-preset-selector.tsx +79 -0
- package/src/components/agents/personality-builder.tsx +111 -0
- package/src/components/agents/sandbox-env-panel.tsx +72 -0
- package/src/components/agents/skill-install-dialog.tsx +102 -0
- package/src/components/agents/trash-list.tsx +109 -0
- package/src/components/chat/chat-area.tsx +41 -6
- package/src/components/chat/chat-header.tsx +305 -29
- package/src/components/chat/chat-preview-panel.tsx +113 -0
- package/src/components/chat/exec-approval-card.tsx +89 -0
- package/src/components/chat/message-bubble.tsx +218 -36
- package/src/components/chat/message-list.tsx +135 -31
- package/src/components/chat/streaming-bubble.tsx +59 -10
- package/src/components/chat/suggestions-bar.tsx +74 -0
- package/src/components/chat/thinking-indicator.tsx +20 -6
- package/src/components/chat/tool-call-bubble.tsx +98 -19
- package/src/components/chat/tool-request-banner.tsx +20 -2
- package/src/components/chat/trace-block.tsx +103 -0
- package/src/components/chat/voice-overlay.tsx +80 -0
- package/src/components/connectors/connector-list.tsx +6 -2
- package/src/components/connectors/connector-sheet.tsx +31 -7
- package/src/components/layout/app-layout.tsx +47 -25
- package/src/components/projects/project-list.tsx +123 -0
- package/src/components/projects/project-sheet.tsx +135 -0
- package/src/components/schedules/schedule-list.tsx +3 -1
- package/src/components/sessions/new-session-sheet.tsx +6 -6
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/sessions/session-list.tsx +7 -7
- package/src/components/settings/gateway-connection-panel.tsx +278 -0
- package/src/components/shared/avatar.tsx +13 -2
- package/src/components/shared/connector-platform-icon.tsx +4 -0
- package/src/components/shared/settings/section-heartbeat.tsx +1 -1
- package/src/components/shared/settings/section-orchestrator.tsx +1 -2
- package/src/components/shared/settings/section-web-search.tsx +56 -0
- package/src/components/shared/settings/settings-page.tsx +74 -0
- package/src/components/skills/skill-list.tsx +2 -1
- package/src/components/tasks/task-board.tsx +1 -1
- package/src/components/tasks/task-list.tsx +5 -2
- package/src/components/tasks/task-sheet.tsx +12 -12
- package/src/hooks/use-continuous-speech.ts +181 -0
- package/src/hooks/use-openclaw-gateway.ts +63 -0
- package/src/hooks/use-view-router.ts +52 -0
- package/src/hooks/use-voice-conversation.ts +80 -0
- package/src/lib/id.ts +6 -0
- package/src/lib/notification-sounds.ts +58 -0
- package/src/lib/personality-parser.ts +97 -0
- package/src/lib/projects.ts +13 -0
- package/src/lib/provider-sets.ts +5 -0
- package/src/lib/providers/anthropic.ts +14 -1
- package/src/lib/providers/index.ts +6 -0
- package/src/lib/providers/ollama.ts +9 -1
- package/src/lib/providers/openai.ts +9 -1
- package/src/lib/providers/openclaw.ts +28 -2
- package/src/lib/runtime-loop.ts +2 -2
- package/src/lib/server/api-routes.test.ts +5 -6
- package/src/lib/server/build-llm.ts +17 -4
- package/src/lib/server/chat-execution.ts +82 -6
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +217 -0
- package/src/lib/server/connectors/bluebubbles.ts +360 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +51 -8
- package/src/lib/server/connectors/manager.ts +424 -13
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +65 -0
- package/src/lib/server/connectors/pairing.test.ts +99 -0
- package/src/lib/server/connectors/pairing.ts +256 -0
- package/src/lib/server/connectors/signal.ts +1 -0
- package/src/lib/server/connectors/teams.ts +5 -5
- package/src/lib/server/connectors/types.ts +10 -0
- package/src/lib/server/daemon-state.ts +11 -0
- package/src/lib/server/execution-log.ts +3 -3
- package/src/lib/server/heartbeat-service.ts +1 -1
- package/src/lib/server/knowledge-db.test.ts +2 -33
- package/src/lib/server/main-agent-loop.ts +8 -9
- package/src/lib/server/main-session.ts +21 -0
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -0
- package/src/lib/server/openclaw-config-sync.ts +107 -0
- package/src/lib/server/openclaw-exec-config.ts +52 -0
- package/src/lib/server/openclaw-gateway.ts +291 -0
- package/src/lib/server/openclaw-history-merge.ts +36 -0
- package/src/lib/server/openclaw-models.ts +56 -0
- package/src/lib/server/openclaw-permission-presets.ts +64 -0
- package/src/lib/server/openclaw-sync.ts +497 -0
- package/src/lib/server/orchestrator-lg.ts +30 -9
- package/src/lib/server/orchestrator.ts +4 -4
- package/src/lib/server/process-manager.ts +2 -2
- package/src/lib/server/queue.ts +24 -11
- package/src/lib/server/scheduler.ts +2 -2
- package/src/lib/server/session-mailbox.ts +2 -2
- package/src/lib/server/session-run-manager.ts +2 -2
- package/src/lib/server/session-tools/connector.ts +53 -6
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +22 -6
- package/src/lib/server/session-tools/file.ts +192 -19
- package/src/lib/server/session-tools/index.ts +4 -2
- package/src/lib/server/session-tools/memory.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
- package/src/lib/server/session-tools/sandbox.ts +33 -0
- package/src/lib/server/session-tools/search-providers.ts +277 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/web.ts +53 -72
- package/src/lib/server/storage.ts +74 -11
- package/src/lib/server/stream-agent-chat.ts +53 -4
- package/src/lib/server/suggestions.ts +20 -0
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/server/ws-hub.ts +14 -0
- package/src/lib/tool-definitions.ts +5 -3
- package/src/lib/tts-stream.ts +130 -0
- package/src/lib/view-routes.ts +28 -0
- package/src/proxy.ts +3 -0
- package/src/stores/use-app-store.ts +80 -1
- package/src/stores/use-approval-store.ts +78 -0
- package/src/stores/use-chat-store.ts +162 -6
- package/src/types/index.ts +154 -3
- package/tsconfig.json +13 -4
|
@@ -0,0 +1,99 @@
|
|
|
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 { test } from 'node:test'
|
|
6
|
+
import {
|
|
7
|
+
addAllowedSender,
|
|
8
|
+
approvePairingCode,
|
|
9
|
+
clearConnectorPairingState,
|
|
10
|
+
createOrTouchPairingRequest,
|
|
11
|
+
isSenderAllowed,
|
|
12
|
+
listPendingPairingRequests,
|
|
13
|
+
listStoredAllowedSenders,
|
|
14
|
+
parseAllowFromCsv,
|
|
15
|
+
parsePairingPolicy,
|
|
16
|
+
} from './pairing.ts'
|
|
17
|
+
|
|
18
|
+
function withTempDataDir<T>(fn: (dir: string) => T): T {
|
|
19
|
+
const original = process.env.DATA_DIR
|
|
20
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-pairing-test-'))
|
|
21
|
+
process.env.DATA_DIR = tempDir
|
|
22
|
+
try {
|
|
23
|
+
return fn(tempDir)
|
|
24
|
+
} finally {
|
|
25
|
+
if (typeof original === 'string') process.env.DATA_DIR = original
|
|
26
|
+
else delete process.env.DATA_DIR
|
|
27
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test('pairing store creates request, approves code, and persists allowlist', () => {
|
|
32
|
+
withTempDataDir(() => {
|
|
33
|
+
const connectorId = 'pair-test-1'
|
|
34
|
+
|
|
35
|
+
const first = createOrTouchPairingRequest({
|
|
36
|
+
connectorId,
|
|
37
|
+
senderId: '+15551234567',
|
|
38
|
+
senderName: 'Alice',
|
|
39
|
+
channelId: 'chat:1',
|
|
40
|
+
})
|
|
41
|
+
assert.equal(first.created, true)
|
|
42
|
+
assert.equal(first.code.length, 8)
|
|
43
|
+
|
|
44
|
+
const second = createOrTouchPairingRequest({
|
|
45
|
+
connectorId,
|
|
46
|
+
senderId: '+15551234567',
|
|
47
|
+
senderName: 'Alice',
|
|
48
|
+
channelId: 'chat:1',
|
|
49
|
+
})
|
|
50
|
+
assert.equal(second.created, false)
|
|
51
|
+
assert.equal(second.code, first.code)
|
|
52
|
+
|
|
53
|
+
const pendingBefore = listPendingPairingRequests(connectorId)
|
|
54
|
+
assert.equal(pendingBefore.length, 1)
|
|
55
|
+
assert.equal(pendingBefore[0].senderId, '+15551234567')
|
|
56
|
+
|
|
57
|
+
const bad = approvePairingCode(connectorId, 'INVALID')
|
|
58
|
+
assert.equal(bad.ok, false)
|
|
59
|
+
|
|
60
|
+
const approved = approvePairingCode(connectorId, first.code)
|
|
61
|
+
assert.equal(approved.ok, true)
|
|
62
|
+
assert.equal(approved.senderId, '+15551234567')
|
|
63
|
+
|
|
64
|
+
const pendingAfter = listPendingPairingRequests(connectorId)
|
|
65
|
+
assert.equal(pendingAfter.length, 0)
|
|
66
|
+
|
|
67
|
+
const stored = listStoredAllowedSenders(connectorId)
|
|
68
|
+
assert.deepEqual(stored, ['+15551234567'])
|
|
69
|
+
|
|
70
|
+
assert.equal(isSenderAllowed({ connectorId, senderId: '+15551234567' }), true)
|
|
71
|
+
assert.equal(isSenderAllowed({ connectorId, senderId: '+16667778888' }), false)
|
|
72
|
+
|
|
73
|
+
clearConnectorPairingState(connectorId)
|
|
74
|
+
assert.deepEqual(listStoredAllowedSenders(connectorId), [])
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('pairing helpers normalize policy and allowFrom csv entries', () => {
|
|
79
|
+
assert.equal(parsePairingPolicy('PAIRING'), 'pairing')
|
|
80
|
+
assert.equal(parsePairingPolicy('allowlist'), 'allowlist')
|
|
81
|
+
assert.equal(parsePairingPolicy('unknown', 'open'), 'open')
|
|
82
|
+
|
|
83
|
+
const list = parseAllowFromCsv(' +1555,TEST@example.com,+1555 , ')
|
|
84
|
+
assert.deepEqual(list, ['+1555', 'test@example.com'])
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('addAllowedSender deduplicates and normalizes sender ids', () => {
|
|
88
|
+
withTempDataDir(() => {
|
|
89
|
+
const connectorId = 'pair-test-2'
|
|
90
|
+
const first = addAllowedSender(connectorId, ' TEST@Example.com ')
|
|
91
|
+
assert.equal(first.added, true)
|
|
92
|
+
assert.equal(first.normalized, 'test@example.com')
|
|
93
|
+
|
|
94
|
+
const second = addAllowedSender(connectorId, 'test@example.com')
|
|
95
|
+
assert.equal(second.added, false)
|
|
96
|
+
|
|
97
|
+
assert.deepEqual(listStoredAllowedSenders(connectorId), ['test@example.com'])
|
|
98
|
+
})
|
|
99
|
+
})
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
const DEFAULT_DATA_DIR = path.join(process.cwd(), 'data')
|
|
6
|
+
const STORE_VERSION = 1
|
|
7
|
+
const PENDING_TTL_MS = 24 * 60 * 60 * 1000
|
|
8
|
+
const MAX_PENDING_PER_CONNECTOR = 100
|
|
9
|
+
const PAIR_CODE_LENGTH = 8
|
|
10
|
+
const PAIR_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
|
11
|
+
|
|
12
|
+
function resolveStorePath(): string {
|
|
13
|
+
const dataDir = process.env.DATA_DIR || DEFAULT_DATA_DIR
|
|
14
|
+
return path.join(dataDir, 'connectors', 'pairing-store.json')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type PairingPolicy = 'open' | 'allowlist' | 'pairing' | 'disabled'
|
|
18
|
+
|
|
19
|
+
export interface PairingRequest {
|
|
20
|
+
code: string
|
|
21
|
+
senderId: string
|
|
22
|
+
senderName?: string
|
|
23
|
+
channelId?: string
|
|
24
|
+
createdAt: number
|
|
25
|
+
updatedAt: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ConnectorPairingState {
|
|
29
|
+
allowedSenderIds: string[]
|
|
30
|
+
pending: PairingRequest[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface PairingStore {
|
|
34
|
+
version: number
|
|
35
|
+
connectors: Record<string, ConnectorPairingState>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeSenderId(value: string): string {
|
|
39
|
+
return value.trim().toLowerCase()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function dedupe(items: string[]): string[] {
|
|
43
|
+
const seen = new Set<string>()
|
|
44
|
+
const out: string[] = []
|
|
45
|
+
for (const item of items) {
|
|
46
|
+
const normalized = normalizeSenderId(item)
|
|
47
|
+
if (!normalized || seen.has(normalized)) continue
|
|
48
|
+
seen.add(normalized)
|
|
49
|
+
out.push(normalized)
|
|
50
|
+
}
|
|
51
|
+
return out
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function prunePending(entries: PairingRequest[]): PairingRequest[] {
|
|
55
|
+
const now = Date.now()
|
|
56
|
+
return entries.filter((entry) => {
|
|
57
|
+
if (!entry?.code || !entry?.senderId) return false
|
|
58
|
+
if (!Number.isFinite(entry.createdAt) || !Number.isFinite(entry.updatedAt)) return false
|
|
59
|
+
return (now - entry.updatedAt) <= PENDING_TTL_MS
|
|
60
|
+
}).slice(-MAX_PENDING_PER_CONNECTOR)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function emptyStore(): PairingStore {
|
|
64
|
+
return { version: STORE_VERSION, connectors: {} }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function loadStore(): PairingStore {
|
|
68
|
+
const storePath = resolveStorePath()
|
|
69
|
+
try {
|
|
70
|
+
if (!fs.existsSync(storePath)) return emptyStore()
|
|
71
|
+
const raw = fs.readFileSync(storePath, 'utf8')
|
|
72
|
+
const parsed = JSON.parse(raw) as PairingStore
|
|
73
|
+
if (!parsed || typeof parsed !== 'object' || typeof parsed.connectors !== 'object') {
|
|
74
|
+
return emptyStore()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const normalized: PairingStore = emptyStore()
|
|
78
|
+
for (const [connectorId, value] of Object.entries(parsed.connectors || {})) {
|
|
79
|
+
const state = value as Partial<ConnectorPairingState>
|
|
80
|
+
const allowedSenderIds = dedupe(Array.isArray(state.allowedSenderIds) ? state.allowedSenderIds.map(String) : [])
|
|
81
|
+
const pending = prunePending(Array.isArray(state.pending) ? state.pending as PairingRequest[] : [])
|
|
82
|
+
normalized.connectors[connectorId] = { allowedSenderIds, pending }
|
|
83
|
+
}
|
|
84
|
+
return normalized
|
|
85
|
+
} catch {
|
|
86
|
+
return emptyStore()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function saveStore(store: PairingStore): void {
|
|
91
|
+
const storePath = resolveStorePath()
|
|
92
|
+
fs.mkdirSync(path.dirname(storePath), { recursive: true })
|
|
93
|
+
fs.writeFileSync(storePath, `${JSON.stringify(store, null, 2)}\n`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function ensureConnectorState(store: PairingStore, connectorId: string): ConnectorPairingState {
|
|
97
|
+
const existing = store.connectors[connectorId]
|
|
98
|
+
if (existing) {
|
|
99
|
+
existing.allowedSenderIds = dedupe(existing.allowedSenderIds || [])
|
|
100
|
+
existing.pending = prunePending(existing.pending || [])
|
|
101
|
+
return existing
|
|
102
|
+
}
|
|
103
|
+
const created: ConnectorPairingState = {
|
|
104
|
+
allowedSenderIds: [],
|
|
105
|
+
pending: [],
|
|
106
|
+
}
|
|
107
|
+
store.connectors[connectorId] = created
|
|
108
|
+
return created
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function randomPairCode(existing: Set<string>): string {
|
|
112
|
+
for (let i = 0; i < 256; i++) {
|
|
113
|
+
const bytes = crypto.randomBytes(PAIR_CODE_LENGTH)
|
|
114
|
+
let out = ''
|
|
115
|
+
for (let j = 0; j < PAIR_CODE_LENGTH; j++) {
|
|
116
|
+
out += PAIR_CODE_ALPHABET[bytes[j] % PAIR_CODE_ALPHABET.length]
|
|
117
|
+
}
|
|
118
|
+
if (!existing.has(out)) return out
|
|
119
|
+
}
|
|
120
|
+
throw new Error('Unable to generate unique pairing code')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function parsePairingPolicy(value: unknown, fallback: PairingPolicy = 'open'): PairingPolicy {
|
|
124
|
+
if (typeof value !== 'string') return fallback
|
|
125
|
+
const normalized = value.trim().toLowerCase()
|
|
126
|
+
if (normalized === 'open' || normalized === 'allowlist' || normalized === 'pairing' || normalized === 'disabled') {
|
|
127
|
+
return normalized
|
|
128
|
+
}
|
|
129
|
+
return fallback
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function parseAllowFromCsv(value: unknown): string[] {
|
|
133
|
+
if (typeof value !== 'string') return []
|
|
134
|
+
return dedupe(value.split(',').map((item) => item.trim()).filter(Boolean))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function listStoredAllowedSenders(connectorId: string): string[] {
|
|
138
|
+
const store = loadStore()
|
|
139
|
+
const state = ensureConnectorState(store, connectorId)
|
|
140
|
+
return state.allowedSenderIds.slice()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function listPendingPairingRequests(connectorId: string): PairingRequest[] {
|
|
144
|
+
const store = loadStore()
|
|
145
|
+
const state = ensureConnectorState(store, connectorId)
|
|
146
|
+
return state.pending.slice().sort((a, b) => b.updatedAt - a.updatedAt)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function addAllowedSender(connectorId: string, senderId: string): { added: boolean; normalized: string } {
|
|
150
|
+
const normalized = normalizeSenderId(senderId)
|
|
151
|
+
if (!normalized) return { added: false, normalized }
|
|
152
|
+
|
|
153
|
+
const store = loadStore()
|
|
154
|
+
const state = ensureConnectorState(store, connectorId)
|
|
155
|
+
const hasExisting = state.allowedSenderIds.includes(normalized)
|
|
156
|
+
if (!hasExisting) {
|
|
157
|
+
state.allowedSenderIds.push(normalized)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Remove any pending requests for the same sender after approval.
|
|
161
|
+
state.pending = state.pending.filter((entry) => normalizeSenderId(entry.senderId) !== normalized)
|
|
162
|
+
|
|
163
|
+
saveStore(store)
|
|
164
|
+
return { added: !hasExisting, normalized }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function createOrTouchPairingRequest(params: {
|
|
168
|
+
connectorId: string
|
|
169
|
+
senderId: string
|
|
170
|
+
senderName?: string
|
|
171
|
+
channelId?: string
|
|
172
|
+
}): { code: string; created: boolean } {
|
|
173
|
+
const normalized = normalizeSenderId(params.senderId)
|
|
174
|
+
if (!normalized) throw new Error('senderId is required')
|
|
175
|
+
|
|
176
|
+
const store = loadStore()
|
|
177
|
+
const state = ensureConnectorState(store, params.connectorId)
|
|
178
|
+
const now = Date.now()
|
|
179
|
+
|
|
180
|
+
const existing = state.pending.find((entry) => normalizeSenderId(entry.senderId) === normalized)
|
|
181
|
+
if (existing) {
|
|
182
|
+
existing.updatedAt = now
|
|
183
|
+
existing.senderName = params.senderName || existing.senderName
|
|
184
|
+
existing.channelId = params.channelId || existing.channelId
|
|
185
|
+
saveStore(store)
|
|
186
|
+
return { code: existing.code, created: false }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const existingCodes = new Set(state.pending.map((entry) => entry.code.toUpperCase()))
|
|
190
|
+
const code = randomPairCode(existingCodes)
|
|
191
|
+
state.pending.push({
|
|
192
|
+
code,
|
|
193
|
+
senderId: normalized,
|
|
194
|
+
senderName: params.senderName,
|
|
195
|
+
channelId: params.channelId,
|
|
196
|
+
createdAt: now,
|
|
197
|
+
updatedAt: now,
|
|
198
|
+
})
|
|
199
|
+
state.pending = prunePending(state.pending)
|
|
200
|
+
saveStore(store)
|
|
201
|
+
return { code, created: true }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function approvePairingCode(connectorId: string, codeRaw: string): {
|
|
205
|
+
ok: boolean
|
|
206
|
+
senderId?: string
|
|
207
|
+
senderName?: string
|
|
208
|
+
reason?: string
|
|
209
|
+
} {
|
|
210
|
+
const code = codeRaw.trim().toUpperCase()
|
|
211
|
+
if (!code) return { ok: false, reason: 'Missing code' }
|
|
212
|
+
|
|
213
|
+
const store = loadStore()
|
|
214
|
+
const state = ensureConnectorState(store, connectorId)
|
|
215
|
+
const idx = state.pending.findIndex((entry) => entry.code.toUpperCase() === code)
|
|
216
|
+
if (idx < 0) return { ok: false, reason: 'Code not found or expired' }
|
|
217
|
+
|
|
218
|
+
const pending = state.pending[idx]
|
|
219
|
+
state.pending.splice(idx, 1)
|
|
220
|
+
|
|
221
|
+
const normalizedSender = normalizeSenderId(pending.senderId)
|
|
222
|
+
if (!state.allowedSenderIds.includes(normalizedSender)) {
|
|
223
|
+
state.allowedSenderIds.push(normalizedSender)
|
|
224
|
+
state.allowedSenderIds = dedupe(state.allowedSenderIds)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
saveStore(store)
|
|
228
|
+
return {
|
|
229
|
+
ok: true,
|
|
230
|
+
senderId: normalizedSender,
|
|
231
|
+
senderName: pending.senderName,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function isSenderAllowed(params: {
|
|
236
|
+
connectorId: string
|
|
237
|
+
senderId: string
|
|
238
|
+
configAllowFrom?: string[]
|
|
239
|
+
}): boolean {
|
|
240
|
+
const normalized = normalizeSenderId(params.senderId)
|
|
241
|
+
if (!normalized) return false
|
|
242
|
+
|
|
243
|
+
const configSet = new Set((params.configAllowFrom || []).map((item) => normalizeSenderId(item)).filter(Boolean))
|
|
244
|
+
if (configSet.has(normalized)) return true
|
|
245
|
+
|
|
246
|
+
const store = loadStore()
|
|
247
|
+
const state = ensureConnectorState(store, params.connectorId)
|
|
248
|
+
return state.allowedSenderIds.includes(normalized)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function clearConnectorPairingState(connectorId: string): void {
|
|
252
|
+
const store = loadStore()
|
|
253
|
+
if (!store.connectors[connectorId]) return
|
|
254
|
+
delete store.connectors[connectorId]
|
|
255
|
+
saveStore(store)
|
|
256
|
+
}
|
|
@@ -22,11 +22,11 @@ const teams: PlatformConnector = {
|
|
|
22
22
|
const conversationReferences = new Map<string, any>()
|
|
23
23
|
let stopped = false
|
|
24
24
|
|
|
25
|
-
// Process incoming activities — called from the webhook endpoint
|
|
26
|
-
//
|
|
27
|
-
const processActivity = async (
|
|
25
|
+
// Process incoming activities — called from the webhook endpoint.
|
|
26
|
+
// We use processActivityDirect so this works from Next.js route handlers.
|
|
27
|
+
const processActivity = async (activity: any) => {
|
|
28
28
|
if (stopped) return
|
|
29
|
-
await adapter.
|
|
29
|
+
await adapter.processActivityDirect(activity, async (context: any) => {
|
|
30
30
|
if (context.activity.type !== 'message') return
|
|
31
31
|
if (!context.activity.text) return
|
|
32
32
|
|
|
@@ -57,7 +57,7 @@ const teams: PlatformConnector = {
|
|
|
57
57
|
})
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
// Store processActivity on globalThis so the webhook route can access it
|
|
60
|
+
// Store processActivity on globalThis so the webhook route can access it.
|
|
61
61
|
const handlerKey = `__swarmclaw_teams_handler_${connector.id}__`
|
|
62
62
|
;(globalThis as any)[handlerKey] = processActivity
|
|
63
63
|
|
|
@@ -21,9 +21,11 @@ export interface InboundMessage {
|
|
|
21
21
|
senderId: string // platform-specific user ID
|
|
22
22
|
senderName: string // display name
|
|
23
23
|
text: string
|
|
24
|
+
isGroup?: boolean
|
|
24
25
|
imageUrl?: string
|
|
25
26
|
media?: InboundMedia[]
|
|
26
27
|
replyToMessageId?: string
|
|
28
|
+
agentIdOverride?: string
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/** A running connector instance */
|
|
@@ -50,6 +52,14 @@ export interface ConnectorInstance {
|
|
|
50
52
|
authenticated?: boolean
|
|
51
53
|
/** Whether the connector has existing saved credentials (WhatsApp only) */
|
|
52
54
|
hasCredentials?: boolean
|
|
55
|
+
/** Rich messaging: send a reaction emoji to a message */
|
|
56
|
+
sendReaction?: (channelId: string, messageId: string, emoji: string) => Promise<void>
|
|
57
|
+
/** Rich messaging: edit a previously sent message */
|
|
58
|
+
editMessage?: (channelId: string, messageId: string, newText: string) => Promise<void>
|
|
59
|
+
/** Rich messaging: delete a message */
|
|
60
|
+
deleteMessage?: (channelId: string, messageId: string) => Promise<void>
|
|
61
|
+
/** Rich messaging: pin a message */
|
|
62
|
+
pinMessage?: (channelId: string, messageId: string) => Promise<void>
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
/** Platform-specific connector implementation */
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getConnectorStatus,
|
|
13
13
|
} from './connectors/manager'
|
|
14
14
|
import { startHeartbeatService, stopHeartbeatService, getHeartbeatServiceStatus } from './heartbeat-service'
|
|
15
|
+
import { hasOpenClawAgents, ensureGatewayConnected, disconnectGateway, getGateway } from './openclaw-gateway'
|
|
15
16
|
|
|
16
17
|
const QUEUE_CHECK_INTERVAL = 30_000 // 30 seconds
|
|
17
18
|
const BROWSER_SWEEP_INTERVAL = 60_000 // 60 seconds
|
|
@@ -179,6 +180,16 @@ function startQueueProcessor() {
|
|
|
179
180
|
await processNext()
|
|
180
181
|
ds.lastProcessedAt = Date.now()
|
|
181
182
|
}
|
|
183
|
+
// OpenClaw gateway lifecycle: lazy connect when openclaw agents exist, disconnect when none remain
|
|
184
|
+
try {
|
|
185
|
+
if (hasOpenClawAgents()) {
|
|
186
|
+
if (!getGateway()?.connected) {
|
|
187
|
+
await ensureGatewayConnected()
|
|
188
|
+
}
|
|
189
|
+
} else if (getGateway()?.connected) {
|
|
190
|
+
disconnectGateway()
|
|
191
|
+
}
|
|
192
|
+
} catch { /* gateway errors are non-fatal */ }
|
|
182
193
|
}, QUEUE_CHECK_INTERVAL)
|
|
183
194
|
}
|
|
184
195
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import path from 'path'
|
|
3
|
-
import crypto from 'crypto'
|
|
4
3
|
import Database from 'better-sqlite3'
|
|
4
|
+
import { genId } from '@/lib/id'
|
|
5
5
|
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
// Types
|
|
@@ -94,7 +94,7 @@ export function logExecution(
|
|
|
94
94
|
detail?: Record<string, unknown>
|
|
95
95
|
},
|
|
96
96
|
): string {
|
|
97
|
-
const id =
|
|
97
|
+
const id = genId(8)
|
|
98
98
|
const ts = Date.now()
|
|
99
99
|
try {
|
|
100
100
|
insertStmt().run(
|
|
@@ -133,7 +133,7 @@ export function logExecutionBatch(
|
|
|
133
133
|
const tx = db.transaction(() => {
|
|
134
134
|
for (const e of entries) {
|
|
135
135
|
stmt.run(
|
|
136
|
-
|
|
136
|
+
genId(8),
|
|
137
137
|
e.sessionId,
|
|
138
138
|
e.runId ?? null,
|
|
139
139
|
e.agentId ?? null,
|
|
@@ -158,6 +158,7 @@ function buildAgentHeartbeatPrompt(session: any, agent: any, fallbackPrompt: str
|
|
|
158
158
|
soul ? `Persona: ${soul.slice(0, 300)}` : '',
|
|
159
159
|
heartbeatFileContent ? `\nHEARTBEAT.md contents:\n${heartbeatFileContent.slice(0, 2000)}` : '',
|
|
160
160
|
recentContext ? `Recent conversation:\n${recentContext}` : '',
|
|
161
|
+
fallbackPrompt !== DEFAULT_HEARTBEAT_PROMPT ? `\nAgent instructions:\n${fallbackPrompt}` : '',
|
|
161
162
|
'',
|
|
162
163
|
'You are running an autonomous heartbeat tick. Review your goal and recent context.',
|
|
163
164
|
'If there is meaningful work to do toward your goal, use your tools and take action.',
|
|
@@ -167,7 +168,6 @@ function buildAgentHeartbeatPrompt(session: any, agent: any, fallbackPrompt: str
|
|
|
167
168
|
'To update your goal or plan, include this line in your response:',
|
|
168
169
|
'[AGENT_HEARTBEAT_META]{"goal": "your evolved goal", "status": "progress", "next_action": "what you plan to do next"}',
|
|
169
170
|
'You can evolve your goal as you learn more. Set status to "progress" while working, "ok" when done, "idle" when waiting.',
|
|
170
|
-
fallbackPrompt !== DEFAULT_HEARTBEAT_PROMPT ? `\nAdditional instructions: ${fallbackPrompt}` : '',
|
|
171
171
|
].filter(Boolean).join('\n')
|
|
172
172
|
}
|
|
173
173
|
|
|
@@ -104,39 +104,8 @@ function rowToEntry(row: Record<string, unknown>): MemoryEntry {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
// ---- Knowledge helpers (
|
|
108
|
-
|
|
109
|
-
const MEMORY_FTS_STOP_WORDS = new Set([
|
|
110
|
-
'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'for', 'from', 'how',
|
|
111
|
-
'i', 'if', 'in', 'is', 'it', 'of', 'on', 'or', 'that', 'the', 'this',
|
|
112
|
-
'to', 'was', 'we', 'were', 'what', 'when', 'where', 'which', 'who', 'with',
|
|
113
|
-
'you', 'your',
|
|
114
|
-
])
|
|
115
|
-
const MAX_FTS_QUERY_TERMS = 6
|
|
116
|
-
const MAX_FTS_TERM_LENGTH = 48
|
|
117
|
-
|
|
118
|
-
function buildFtsQuery(input: string): string {
|
|
119
|
-
const tokens = String(input || '')
|
|
120
|
-
.toLowerCase()
|
|
121
|
-
.match(/[a-z0-9][a-z0-9._:/-]*/g) || []
|
|
122
|
-
if (!tokens.length) return ''
|
|
123
|
-
const unique: string[] = []
|
|
124
|
-
const seen = new Set<string>()
|
|
125
|
-
for (const token of tokens) {
|
|
126
|
-
const term = token.slice(0, MAX_FTS_TERM_LENGTH)
|
|
127
|
-
if (term.length < 3) continue
|
|
128
|
-
if (MEMORY_FTS_STOP_WORDS.has(term)) continue
|
|
129
|
-
if (seen.has(term)) continue
|
|
130
|
-
seen.add(term)
|
|
131
|
-
unique.push(term)
|
|
132
|
-
if (unique.length >= MAX_FTS_QUERY_TERMS) break
|
|
133
|
-
}
|
|
134
|
-
if (unique.length === 1) {
|
|
135
|
-
return unique[0].length >= 5 ? `"${unique[0].replace(/"/g, '')}"` : ''
|
|
136
|
-
}
|
|
137
|
-
const selected = unique.slice(0, Math.min(4, MAX_FTS_QUERY_TERMS))
|
|
138
|
-
return selected.map((term) => `"${term.replace(/"/g, '')}"`).join(' AND ')
|
|
139
|
-
}
|
|
107
|
+
// ---- Knowledge helpers (re-exported from memory-db.ts) ----
|
|
108
|
+
import { buildFtsQuery } from './memory-db'
|
|
140
109
|
|
|
141
110
|
function addRawMemory(data: {
|
|
142
111
|
agentId?: string | null
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import type { GoalContract, MessageToolEvent } from '@/types'
|
|
4
4
|
import { loadSessions, saveSessions, loadAgents, saveAgents, loadTasks, saveTasks } from './storage'
|
|
5
5
|
import { log } from './logger'
|
|
6
6
|
import { getMemoryDb } from './memory-db'
|
|
7
|
+
import { isProtectedMainSession } from './main-session'
|
|
7
8
|
import {
|
|
8
9
|
mergeGoalContracts,
|
|
9
10
|
parseGoalContractFromText,
|
|
10
11
|
parseMainLoopPlan,
|
|
11
12
|
parseMainLoopReview,
|
|
12
13
|
} from './autonomy-contract'
|
|
13
|
-
|
|
14
|
-
const MAIN_SESSION_NAME = '__main__'
|
|
15
14
|
const MAX_PENDING_EVENTS = 40
|
|
16
15
|
const MAX_TIMELINE_EVENTS = 80
|
|
17
16
|
const EVENT_TTL_MS = 7 * 24 * 60 * 60 * 1000
|
|
@@ -151,7 +150,7 @@ function appendTimeline(
|
|
|
151
150
|
const recent = state.timeline.at(-1)
|
|
152
151
|
if (recent && recent.source === source && recent.note === normalizedNote && now - recent.at < 45_000) return
|
|
153
152
|
state.timeline.push({
|
|
154
|
-
id: `tl_${
|
|
153
|
+
id: `tl_${genId()}`,
|
|
155
154
|
at: now,
|
|
156
155
|
source,
|
|
157
156
|
note: normalizedNote,
|
|
@@ -226,7 +225,7 @@ function normalizeState(raw: any, now = Date.now()): MainLoopState {
|
|
|
226
225
|
const text = toOneLine(typeof e?.text === 'string' ? e.text : '')
|
|
227
226
|
if (!text) return null
|
|
228
227
|
return {
|
|
229
|
-
id: typeof e?.id === 'string' && e.id.trim() ? e.id.trim() : `evt_${
|
|
228
|
+
id: typeof e?.id === 'string' && e.id.trim() ? e.id.trim() : `evt_${genId(3)}`,
|
|
230
229
|
type: typeof e?.type === 'string' && e.type.trim() ? e.type.trim() : 'event',
|
|
231
230
|
text,
|
|
232
231
|
createdAt: typeof e?.createdAt === 'number' ? e.createdAt : now,
|
|
@@ -246,7 +245,7 @@ function normalizeState(raw: any, now = Date.now()): MainLoopState {
|
|
|
246
245
|
? entry.status
|
|
247
246
|
: undefined
|
|
248
247
|
return {
|
|
249
|
-
id: typeof entry?.id === 'string' && entry.id.trim() ? entry.id.trim() : `tl_${
|
|
248
|
+
id: typeof entry?.id === 'string' && entry.id.trim() ? entry.id.trim() : `tl_${genId(3)}`,
|
|
250
249
|
at: typeof entry?.at === 'number' ? entry.at : now,
|
|
251
250
|
source: typeof entry?.source === 'string' && entry.source.trim() ? entry.source.trim() : 'event',
|
|
252
251
|
note,
|
|
@@ -303,7 +302,7 @@ function appendEvent(state: MainLoopState, type: string, text: string, now = Dat
|
|
|
303
302
|
return false
|
|
304
303
|
}
|
|
305
304
|
state.pendingEvents.push({
|
|
306
|
-
id: `evt_${
|
|
305
|
+
id: `evt_${genId()}`,
|
|
307
306
|
type,
|
|
308
307
|
text: normalizedText,
|
|
309
308
|
createdAt: now,
|
|
@@ -517,7 +516,7 @@ function upsertMissionTask(session: any, state: MainLoopState, now: number): str
|
|
|
517
516
|
].filter(Boolean).join('\n')
|
|
518
517
|
|
|
519
518
|
if (!task) {
|
|
520
|
-
const id =
|
|
519
|
+
const id = genId()
|
|
521
520
|
task = {
|
|
522
521
|
id,
|
|
523
522
|
title,
|
|
@@ -669,7 +668,7 @@ function buildFollowupPrompt(state: MainLoopState, opts?: { hasMemoryTool?: bool
|
|
|
669
668
|
}
|
|
670
669
|
|
|
671
670
|
export function isMainSession(session: any): boolean {
|
|
672
|
-
return session
|
|
671
|
+
return isProtectedMainSession(session)
|
|
673
672
|
}
|
|
674
673
|
|
|
675
674
|
export function buildMainLoopHeartbeatPrompt(session: any, fallbackPrompt: string): string {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const MAIN_SESSION_NAME = '__main__'
|
|
2
|
+
|
|
3
|
+
export function isProtectedMainSession(session: any): boolean {
|
|
4
|
+
if (!session || typeof session !== 'object') return false
|
|
5
|
+
if (session.mainSession === true) return true
|
|
6
|
+
|
|
7
|
+
const name = typeof session.name === 'string' ? session.name.trim() : ''
|
|
8
|
+
if (name === MAIN_SESSION_NAME) return true
|
|
9
|
+
|
|
10
|
+
const id = typeof session.id === 'string' ? session.id.trim() : ''
|
|
11
|
+
if (id.startsWith('main-')) return true
|
|
12
|
+
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ensureMainSessionFlag(session: any): void {
|
|
17
|
+
if (!session || typeof session !== 'object') return
|
|
18
|
+
if (isProtectedMainSession(session)) {
|
|
19
|
+
session.mainSession = true
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Database from 'better-sqlite3'
|
|
2
2
|
import path from 'path'
|
|
3
|
-
import crypto from 'crypto'
|
|
4
3
|
import fs from 'fs'
|
|
4
|
+
import { genId } from '@/lib/id'
|
|
5
5
|
import type { MemoryEntry, FileReference, MemoryImage, MemoryReference } from '@/types'
|
|
6
6
|
import { getEmbedding, cosineSimilarity, serializeEmbedding, deserializeEmbedding } from './embeddings'
|
|
7
7
|
import { loadSettings } from './storage'
|
|
@@ -20,12 +20,12 @@ const IMAGES_DIR = path.join(DATA_DIR, 'memory-images')
|
|
|
20
20
|
|
|
21
21
|
const MAX_IMAGE_INPUT_BYTES = 10 * 1024 * 1024 // 10MB
|
|
22
22
|
const IMAGE_EXT_WHITELIST = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.tiff'])
|
|
23
|
-
const MAX_FTS_QUERY_TERMS = 6
|
|
24
|
-
const MAX_FTS_TERM_LENGTH = 48
|
|
23
|
+
export const MAX_FTS_QUERY_TERMS = 6
|
|
24
|
+
export const MAX_FTS_TERM_LENGTH = 48
|
|
25
25
|
const MAX_FTS_RESULT_ROWS = 30
|
|
26
26
|
const MAX_MERGED_RESULTS = 50
|
|
27
27
|
|
|
28
|
-
const MEMORY_FTS_STOP_WORDS = new Set([
|
|
28
|
+
export const MEMORY_FTS_STOP_WORDS = new Set([
|
|
29
29
|
'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'for', 'from', 'how',
|
|
30
30
|
'i', 'if', 'in', 'is', 'it', 'of', 'on', 'or', 'that', 'the', 'this',
|
|
31
31
|
'to', 'was', 'we', 'were', 'what', 'when', 'where', 'which', 'who', 'with',
|
|
@@ -184,7 +184,7 @@ function canonicalText(value: unknown): string {
|
|
|
184
184
|
.trim()
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
function buildFtsQuery(input: string): string {
|
|
187
|
+
export function buildFtsQuery(input: string): string {
|
|
188
188
|
const tokens = String(input || '')
|
|
189
189
|
.toLowerCase()
|
|
190
190
|
.match(/[a-z0-9][a-z0-9._:/-]*/g) || []
|
|
@@ -546,7 +546,7 @@ function initDb() {
|
|
|
546
546
|
|
|
547
547
|
return {
|
|
548
548
|
add(data: Omit<MemoryEntry, 'id' | 'createdAt' | 'updatedAt'>): MemoryEntry {
|
|
549
|
-
const id =
|
|
549
|
+
const id = genId(6)
|
|
550
550
|
const now = Date.now()
|
|
551
551
|
const references = normalizeReferences(data.references, data.filePaths)
|
|
552
552
|
const legacyFilePaths = referencesToLegacyFilePaths(references)
|