ds-bot 0.1.3 → 0.1.5
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 +5 -4
- package/client.js +1948 -1269
- package/index.js +12 -3
- package/package.json +1 -1
- package/src/host/approvals.js +8 -2
- package/src/host/capabilities.js +521 -0
- package/src/host/channel.js +25 -6
- package/src/host/constants.js +11 -0
- package/src/host/delivery.js +21 -6
- package/src/host/folders.js +135 -0
- package/src/host/groups.js +123 -33
- package/src/host/images.js +2 -2
- package/src/host/memory.js +31 -8
- package/src/host/parts.js +100 -32
- package/src/host/prompt.js +5 -5
- package/src/host/resume.js +247 -0
- package/src/host/roster.js +41 -10
- package/src/host/secrets.js +9 -1
- package/src/host/store.js +16 -3
- package/src/host/tools.js +193 -19
- package/src/host/turns.js +126 -14
- package/src/host/usage.js +4 -1
package/src/host/channel.js
CHANGED
|
@@ -40,7 +40,7 @@ export function install(rt) {
|
|
|
40
40
|
...bot, sessionId: rt.chatOf(bot.id), parts: (segments ?? []).map(segment => segment.sessionId), ...(kickoff ? { waiting: true } : {}),
|
|
41
41
|
})),
|
|
42
42
|
// `parts`: the members' hidden group Sessions and their earlier segments, for usage.
|
|
43
|
-
rooms: Object.values(state.rooms).map(({ log, seen, botRounds, sessions, segments, ...room }) => {
|
|
43
|
+
rooms: Object.values(state.rooms).map(({ log, seen, botRounds, lateRounds, sessions, segments, ...room }) => {
|
|
44
44
|
const parts = [...Object.values(sessions ?? {}), ...Object.values(segments ?? {}).flat().map(segment => segment.sessionId)]
|
|
45
45
|
const last = (log ?? []).filter(line => line.kind === undefined).at(-1)
|
|
46
46
|
return { ...room, parts, ...(last ? { preview: last.who === 'User' ? last.text : `${last.who}:${last.text}` } : {}) }
|
|
@@ -51,6 +51,7 @@ export function install(rt) {
|
|
|
51
51
|
agents: Object.values(state.agents),
|
|
52
52
|
// Key names and who may use them; values never leave the host.
|
|
53
53
|
secrets: rt.secretsView(),
|
|
54
|
+
capabilities: rt.capabilityLibraryView(),
|
|
54
55
|
secretRequests: state.secretRequests ?? {},
|
|
55
56
|
colors: [...COLORS, ...EXTRA_COLORS],
|
|
56
57
|
models: rt.cachedCatalog(),
|
|
@@ -67,13 +68,15 @@ export function install(rt) {
|
|
|
67
68
|
})
|
|
68
69
|
// What changes without a new revision, sent with every state answer. `memoryNews`:
|
|
69
70
|
// per Bot, its latest memory change; `memoryReviews`: memory reviews waiting or
|
|
70
|
-
// running; `work`: per Bot, the turn it runs and its tool,
|
|
71
|
+
// running; `work`: per Bot, the turn it runs and its tool; per group, the members whose
|
|
72
|
+
// turn there runs; and the approvals waiting.
|
|
71
73
|
const live = () => ({
|
|
72
74
|
activity: rt.activityView(),
|
|
73
|
-
work: { bots: rt.workView(), approvals: rt.approvalsView() },
|
|
75
|
+
work: { bots: rt.workView(), rooms: rt.roomWorkView(), approvals: rt.approvalsView() },
|
|
74
76
|
memoryNews: rt.memoryNews(),
|
|
75
77
|
memoryReviews: rt.memoryReviews(),
|
|
76
78
|
connectors: rt.connectorsView(),
|
|
79
|
+
capabilityStatuses: rt.capabilityStatusesView(),
|
|
77
80
|
})
|
|
78
81
|
// What a host that only reads the team may still answer.
|
|
79
82
|
// Updates change the dsh profile, not the team, so a reading host may run them too.
|
|
@@ -97,7 +100,7 @@ export function install(rt) {
|
|
|
97
100
|
return rt.save()
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
const handle = async (endpoint, payload) => {
|
|
103
|
+
const handle = async (endpoint, payload, signal) => {
|
|
101
104
|
try {
|
|
102
105
|
const body = payload ?? {}
|
|
103
106
|
await rt.refresh()
|
|
@@ -138,7 +141,7 @@ export function install(rt) {
|
|
|
138
141
|
return ok([...rt.roomProgress.entries()].filter(([, entry]) => entry.roomId === body.roomId).map(([callId, entry]) => ({ callId, replies: entry.replies, speaking: entry.speaking ?? null })))
|
|
139
142
|
case 'create-bot': {
|
|
140
143
|
await rt.bootstrap()
|
|
141
|
-
const bot = await rt.createBot({ name: body.name, role: body.role, color: body.color, instructions: body.instructions, brief: body.brief, avatar: body.avatar, model: body.model, effort: body.effort })
|
|
144
|
+
const bot = await rt.createBot({ name: body.name, role: body.role, color: body.color, instructions: body.instructions, brief: body.brief, avatar: body.avatar, model: body.model, effort: body.effort, workDir: body.workDir })
|
|
142
145
|
return ok(bot)
|
|
143
146
|
}
|
|
144
147
|
case 'update-bot':
|
|
@@ -171,9 +174,15 @@ export function install(rt) {
|
|
|
171
174
|
members: Array.isArray(body.members) ? body.members : undefined,
|
|
172
175
|
add: Array.isArray(body.add) ? body.add : undefined,
|
|
173
176
|
remove: Array.isArray(body.remove) ? body.remove : undefined,
|
|
177
|
+
workDir: body.workDir,
|
|
174
178
|
})
|
|
175
179
|
return ok({ room: view().rooms.find(entry => entry.id === room.id), changes })
|
|
176
180
|
}
|
|
181
|
+
case 'folders':
|
|
182
|
+
case 'folder-pick':
|
|
183
|
+
case 'folder-list':
|
|
184
|
+
await rt.load()
|
|
185
|
+
return ok(await rt.folderEndpoint(endpoint, body, signal))
|
|
177
186
|
case 'add-agent':
|
|
178
187
|
await rt.load()
|
|
179
188
|
return ok(await rt.addAgent())
|
|
@@ -205,6 +214,8 @@ export function install(rt) {
|
|
|
205
214
|
case 'approval-answer':
|
|
206
215
|
rt.answerApproval(body.id, body.choice)
|
|
207
216
|
return ok(true)
|
|
217
|
+
case 'stop-member-work':
|
|
218
|
+
return ok(await rt.stopMemberWork(String(body.roomId ?? ''), String(body.botId ?? '')))
|
|
208
219
|
case 'dismiss-question':
|
|
209
220
|
await rt.load()
|
|
210
221
|
if (state.questions[body.sessionId] !== undefined) {
|
|
@@ -224,6 +235,12 @@ export function install(rt) {
|
|
|
224
235
|
case 'connector-retry':
|
|
225
236
|
await rt.load()
|
|
226
237
|
return ok(await rt.connectorEndpoint(endpoint, body))
|
|
238
|
+
case 'capability-save':
|
|
239
|
+
case 'capability-delete':
|
|
240
|
+
case 'capability-approve':
|
|
241
|
+
case 'bot-capability':
|
|
242
|
+
await rt.load()
|
|
243
|
+
return ok(await rt.capabilityEndpoint(endpoint, body))
|
|
227
244
|
case 'schedules':
|
|
228
245
|
case 'schedule-create':
|
|
229
246
|
case 'schedule-delete':
|
|
@@ -246,6 +263,7 @@ export function install(rt) {
|
|
|
246
263
|
// Claim the question before queueing so a second answer is refused.
|
|
247
264
|
delete state.questions[sessionId]
|
|
248
265
|
try {
|
|
266
|
+
await rt.ensureCapabilities(sessionId)
|
|
249
267
|
const agent = await rt.resolveAgent(sessionId)
|
|
250
268
|
agent.followup(createUserMessage({ content: [{ type: 'text', text: reply.text }], source: reply.source }))
|
|
251
269
|
await ctx.sessions.flush(agent.session)
|
|
@@ -263,6 +281,7 @@ export function install(rt) {
|
|
|
263
281
|
// A Bot id or an earlier part of its own chat means the current part.
|
|
264
282
|
const past = rt.pastOwners.get(body.sessionId)
|
|
265
283
|
const sessionId = rt.botOf(body.sessionId) ? rt.chatOf(body.sessionId) : past && past.roomId === undefined ? rt.chatOf(past.botId) : body.sessionId
|
|
284
|
+
await rt.ensureCapabilities(sessionId)
|
|
266
285
|
await ctx.sessionController.prompt({
|
|
267
286
|
requestId: randomUUID(),
|
|
268
287
|
sessionId,
|
|
@@ -319,7 +338,7 @@ export function install(rt) {
|
|
|
319
338
|
fetch: async (request) => {
|
|
320
339
|
let body
|
|
321
340
|
try { body = await request.json() } catch { body = {} }
|
|
322
|
-
const result = await handle(String(body?.endpoint ?? ''), body?.payload)
|
|
341
|
+
const result = await handle(String(body?.endpoint ?? ''), body?.payload, request.signal)
|
|
323
342
|
return new Response(JSON.stringify(result), { status: 200, headers: { 'content-type': 'application/json' } })
|
|
324
343
|
},
|
|
325
344
|
}), 'ds-bot: api route')
|
package/src/host/constants.js
CHANGED
|
@@ -21,6 +21,9 @@ export const OCR_CACHE_MAX = 500
|
|
|
21
21
|
export const MAX_AVATAR_IMAGE = 160_000
|
|
22
22
|
export const ROUTE = '/api/bot'
|
|
23
23
|
export const MAX_HOPS = 6
|
|
24
|
+
// Bot requests a Bot's own chat may hold unread; past this the Bot is most likely
|
|
25
|
+
// stuck, and more messages only pile up behind it.
|
|
26
|
+
export const MAX_UNREAD_REQUESTS = 10
|
|
24
27
|
export const MAX_EXCHANGES = 600
|
|
25
28
|
export const ROOM_REPLY_TIMEOUT_MS = 240_000
|
|
26
29
|
export const SOURCE_KIND = 'bot'
|
|
@@ -49,9 +52,17 @@ export const TEAM_TOOLS = [
|
|
|
49
52
|
'create_group', 'update_group', 'delete_group', 'post_to_group', 'ask_user',
|
|
50
53
|
'request_secret', 'list_secrets', 'group_relay', 'read_own_chat',
|
|
51
54
|
'read_group_chat', 'remember', 'forget', 'recall', 'read_browser',
|
|
55
|
+
'list_capabilities', 'save_capability', 'delete_capability',
|
|
56
|
+
'attach_capability', 'detach_capability',
|
|
52
57
|
]
|
|
53
58
|
// Rounds Bots may start in one group by posting, before the user speaks there again.
|
|
54
59
|
export const ROOM_BOT_ROUNDS = 3
|
|
60
|
+
// Answers a group gives to what members post after the round moved on (their finished
|
|
61
|
+
// work, mostly), before the user speaks there again.
|
|
62
|
+
export const ROOM_LATE_ROUNDS = 10
|
|
63
|
+
// A member still at work this long after its first tool call lets the round go on; it
|
|
64
|
+
// posts its reply when the work is done.
|
|
65
|
+
export const ROOM_WORK_GRACE_MS = 15_000
|
|
55
66
|
|
|
56
67
|
export const OWN_CHAT_LINE_MAX = 400
|
|
57
68
|
// The message an anchor points at is shown whole (up to this), so a long draft takes
|
package/src/host/delivery.js
CHANGED
|
@@ -9,11 +9,15 @@ export function install(rt) {
|
|
|
9
9
|
const resolveAgent = async (sessionId) => {
|
|
10
10
|
const resolved = await ctx.sessionController.resolveAgent(sessionId)
|
|
11
11
|
if ('error' in resolved) throw new Error(resolved.error?.message ?? 'Session unavailable')
|
|
12
|
+
rt.hideBlockingQuestion(resolved.agent)
|
|
12
13
|
return resolved.agent
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
const headerFor = (role, fromName, roomName, audience, note = 'new message', deferred = false) => {
|
|
16
|
+
const headerFor = (role, fromName, roomName, audience, note = 'new message', deferred = false, { resumed = false, retried = false } = {}) => {
|
|
16
17
|
if (deferred) return `[Message from ${fromName} · you read it during your last task; answer it now]`
|
|
18
|
+
if (retried && role !== 'room') return '[Your last reply wrote a tool call as text, so nothing ran]'
|
|
19
|
+
if (resumed && role === 'resume') return '[DeepSeek Harness restarted while you were working]'
|
|
20
|
+
if (resumed && role !== 'room') return `[Message from ${fromName} · DeepSeek Harness restarted while you were working on it]`
|
|
17
21
|
if (role === 'reply') return `[Reply from ${fromName}]`
|
|
18
22
|
if (role === 'stopped') return `[Stopped by ${fromName}]`
|
|
19
23
|
if (role === 'room') return `[Group chat "${roomName}" · ${note}]`
|
|
@@ -29,8 +33,13 @@ export function install(rt) {
|
|
|
29
33
|
|
|
30
34
|
// `sessionId` defaults to the recipient's own chat; group turns name its group Session.
|
|
31
35
|
// `deferred`: a request the Bot already read mid-turn, handed back to answer in a
|
|
32
|
-
// turn of its own; its exchange was recorded when it first arrived.
|
|
33
|
-
|
|
36
|
+
// turn of its own; its exchange was recorded when it first arrived. `resumed` and
|
|
37
|
+
// `retried` carry on a turn that a restart cut off or that wrote its tool call as
|
|
38
|
+
// text (resume.js, turns.js), and `resent` is a message a shutdown cancelled; their
|
|
39
|
+
// exchange is recorded already too. `askedBy` is the member whose @mention called a
|
|
40
|
+
// group turn (groups.js). `fromRoomId`: the group whose Session the sender wrote
|
|
41
|
+
// from; the reply goes back to that Session (turns.js).
|
|
42
|
+
async function deliver({ fromId, toId, sessionId, text, role, hop = 0, replyTo, roomId, audience, note, exchangeId = randomUUID(), steer = false, deferred = false, resumed = false, retried = false, resent = false, askedBy, fromRoomId }) {
|
|
34
43
|
const from = rt.botOf(fromId)
|
|
35
44
|
const to = rt.botOf(toId)
|
|
36
45
|
if (to === undefined) throw new Error(`Unknown Bot ${toId}`)
|
|
@@ -49,16 +58,22 @@ export function install(rt) {
|
|
|
49
58
|
...(roomId ? { roomId } : {}),
|
|
50
59
|
...(audience === 'user' ? { audience } : {}),
|
|
51
60
|
...(deferred ? { deferred: true } : {}),
|
|
61
|
+
...(resumed ? { resumed: true } : {}),
|
|
62
|
+
...(retried ? { retried: true } : {}),
|
|
63
|
+
...(askedBy ? { askedBy } : {}),
|
|
64
|
+
...(fromRoomId ? { senderRoomId: fromRoomId } : {}),
|
|
52
65
|
}
|
|
53
|
-
if (EXCHANGE_ROLES.has(role) && !deferred) {
|
|
66
|
+
if (EXCHANGE_ROLES.has(role) && !deferred && !resumed && !retried && !resent) {
|
|
54
67
|
state.exchanges.push({ id: exchangeId, from: fromId, to: toId, text, role, time: Date.now(), ...(replyTo ? { replyTo } : {}), ...(audience === 'user' ? { audience } : {}) })
|
|
55
68
|
void rt.save().catch(() => {})
|
|
56
69
|
}
|
|
57
|
-
const
|
|
70
|
+
const targetSessionId = sessionId ?? rt.chatOf(toId)
|
|
71
|
+
await rt.ensureCapabilities(targetSessionId)
|
|
72
|
+
const agent = await resolveAgent(targetSessionId)
|
|
58
73
|
// A steered message cuts into a running turn at its next step; a follow-up
|
|
59
74
|
// queues for the next turn (see turns.js for how each is routed).
|
|
60
75
|
const message = createUserMessage({
|
|
61
|
-
content: [{ type: 'text', text: `${headerFor(role, fromName, room?.name, audience, note, deferred)}\n${text}` }],
|
|
76
|
+
content: [{ type: 'text', text: `${headerFor(role, fromName, room?.name, audience, note, deferred, { resumed, retried })}\n${text}` }],
|
|
62
77
|
source,
|
|
63
78
|
})
|
|
64
79
|
if (steer) agent.steer(message)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Working folders: where a Bot's Sessions run. A group's folder holds for every member
|
|
2
|
+
// there, else a Bot works in its own folder, else in the team workspace. A Session's
|
|
3
|
+
// folder is fixed when it is made, so a change moves the conversation on to a fresh
|
|
4
|
+
// part (parts.js).
|
|
5
|
+
import { realpath, stat } from 'node:fs/promises'
|
|
6
|
+
import { homedir } from 'node:os'
|
|
7
|
+
import { basename, isAbsolute, join, resolve, sep } from 'node:path'
|
|
8
|
+
import { unquote } from './text.js'
|
|
9
|
+
|
|
10
|
+
const PICK_TIMEOUT_MS = 10 * 60_000
|
|
11
|
+
|
|
12
|
+
export const within = (inner, outer) => inner === outer || inner.startsWith(outer.endsWith(sep) ? outer : outer + sep)
|
|
13
|
+
|
|
14
|
+
const expandHome = text => (text === '~' ? homedir() : /^~[\\/]/.test(text) ? join(homedir(), text.slice(2)) : text)
|
|
15
|
+
|
|
16
|
+
// Read as DSH reads it, so the team's data and the guarded folder are where DSH keeps
|
|
17
|
+
// its own: a blank value counts as unset.
|
|
18
|
+
export const dshHomeOf = value => resolve(expandHome(value?.trim() ? value : join(homedir(), '.dsh')))
|
|
19
|
+
|
|
20
|
+
export function install(rt) {
|
|
21
|
+
const { ctx, state } = rt
|
|
22
|
+
|
|
23
|
+
const teamFolder = () => ctx.workspaceRegistry.get?.(state.workspaceId)?.path
|
|
24
|
+
const workDirOf = ({ botId, roomId } = {}) => rt.roomOf(roomId)?.workDir ?? rt.botOf(botId)?.workDir
|
|
25
|
+
const isFolder = path => stat(path).then(info => info.isDirectory(), () => false)
|
|
26
|
+
const real = path => realpath(path).catch(() => resolve(path))
|
|
27
|
+
|
|
28
|
+
// A Bot that could write where DSH or the team keeps its data could change its own
|
|
29
|
+
// permissions or the team, so neither those folders nor one that holds them is allowed.
|
|
30
|
+
async function guarded(path) {
|
|
31
|
+
const team = teamFolder()
|
|
32
|
+
for (const root of [rt.dshHome, rt.home]) {
|
|
33
|
+
const data = await real(root)
|
|
34
|
+
if (within(data, path)) return true
|
|
35
|
+
if (within(path, data) && !(team !== undefined && within(path, team))) return true
|
|
36
|
+
}
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Saved as the real path, which is what a Session's cwd shows. Undefined means the
|
|
41
|
+
// team workspace.
|
|
42
|
+
async function cleanWorkDir(input) {
|
|
43
|
+
const text = unquote(input)
|
|
44
|
+
if (text === '') return undefined
|
|
45
|
+
const expanded = expandHome(text)
|
|
46
|
+
if (!isAbsolute(expanded)) throw new Error(`Give the full path of the folder, not ${text}`)
|
|
47
|
+
let path
|
|
48
|
+
try {
|
|
49
|
+
path = await realpath(expanded)
|
|
50
|
+
} catch {
|
|
51
|
+
throw new Error(`There is no folder at ${text}`)
|
|
52
|
+
}
|
|
53
|
+
if (!(await isFolder(path))) throw new Error(`${text} is a file, not a folder`)
|
|
54
|
+
if (await guarded(path)) throw new Error(`${path} holds DeepSeek Harness data, so Bots cannot work there`)
|
|
55
|
+
return path === teamFolder() ? undefined : path
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Every Session of a Bot: its own chat and its conversation in each of its groups.
|
|
59
|
+
const sessionsOfBot = botId => [rt.chatOf(botId), ...Object.values(state.rooms)
|
|
60
|
+
.filter(room => room.members.includes(botId)).map(room => room.sessions?.[botId])].filter(Boolean)
|
|
61
|
+
|
|
62
|
+
// A folder outside the team workspace widens where a Bot may write, so a Bot that
|
|
63
|
+
// sets one for Bots whose writes are bounded by their folder asks the user first.
|
|
64
|
+
// `sessions` are the Sessions that would work there; the default policy counts too,
|
|
65
|
+
// since the Sessions made there later start under it. Throws when it is not allowed.
|
|
66
|
+
async function approveFolder(exec, { path, sessions = [], who }) {
|
|
67
|
+
const team = teamFolder()
|
|
68
|
+
const policy = ctx.get?.('sandboxPolicy')
|
|
69
|
+
if (path === undefined || (team !== undefined && within(path, team)) || policy === undefined) return
|
|
70
|
+
const agents = await Promise.all(sessions.map(id => rt.resolveAgent(id)))
|
|
71
|
+
const modes = [policy.resolve(), ...agents.map(agent => policy.resolve({ session: agent.session }))].map(entry => entry.mode)
|
|
72
|
+
if (!modes.includes('workspace-write')) return
|
|
73
|
+
const approval = ctx.get?.('approval')
|
|
74
|
+
const agent = exec.agent
|
|
75
|
+
if (approval === undefined || agent?.session === undefined) throw new Error(`Working in ${path} needs the user's approval, and there is no way to ask for it here, so nothing changed. The user can make this change in the settings.`)
|
|
76
|
+
const outcome = await approval.request({
|
|
77
|
+
agent,
|
|
78
|
+
toolName: exec.name,
|
|
79
|
+
...(exec.callId !== undefined ? { callId: exec.callId } : {}),
|
|
80
|
+
reason: `Let ${who.en} work in ${path}? Files there can then be changed.`,
|
|
81
|
+
displayReason: { en: `Let ${who.en} work in ${path}? Files there can then be changed.`, zh: `允许${who.zh}在 ${path} 中工作吗?之后可以修改这个文件夹里的文件。` },
|
|
82
|
+
...(exec.signal ? { signal: exec.signal } : {}),
|
|
83
|
+
})
|
|
84
|
+
if (outcome === 'allowed-once') return
|
|
85
|
+
// An approval policy that never asks answers 'rejected' as well.
|
|
86
|
+
throw new Error(outcome === 'rejected' ? `Working in ${path} was not allowed, so nothing changed. The user can make this change in the settings.`
|
|
87
|
+
: outcome === 'cancelled' ? 'The approval was cancelled, so nothing changed.'
|
|
88
|
+
: `Working in ${path} needs the user's approval, and no one could answer, so nothing changed. The user can make this change in the settings.`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const picker = () => {
|
|
92
|
+
try {
|
|
93
|
+
return ctx.get?.('directoryPicker')?.capability()
|
|
94
|
+
} catch {
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function folderEndpoint(endpoint, body, signal) {
|
|
100
|
+
switch (endpoint) {
|
|
101
|
+
case 'folders': {
|
|
102
|
+
const team = teamFolder() ?? null
|
|
103
|
+
const known = new Map()
|
|
104
|
+
for (const workspace of ctx.workspaceRegistry.list?.() ?? []) {
|
|
105
|
+
if (workspace.path !== team) known.set(workspace.path, workspace.title)
|
|
106
|
+
}
|
|
107
|
+
for (const entry of [...Object.values(state.bots), ...Object.values(state.rooms)]) {
|
|
108
|
+
if (entry.workDir && !known.has(entry.workDir)) known.set(entry.workDir, basename(entry.workDir) || entry.workDir)
|
|
109
|
+
}
|
|
110
|
+
const present = await Promise.all([...known].map(async ([path, title]) => (await isFolder(path) ? { path, title } : undefined)))
|
|
111
|
+
const kind = picker()?.kind
|
|
112
|
+
return {
|
|
113
|
+
team,
|
|
114
|
+
picker: kind === 'native' || kind === 'browse' ? kind : null,
|
|
115
|
+
folders: present.filter(Boolean),
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
case 'folder-pick': {
|
|
119
|
+
const native = picker()
|
|
120
|
+
if (native?.kind !== 'native') throw new Error('This DeepSeek Harness has no folder chooser')
|
|
121
|
+
return await native.pick(AbortSignal.any([signal, AbortSignal.timeout(PICK_TIMEOUT_MS)].filter(Boolean)))
|
|
122
|
+
}
|
|
123
|
+
case 'folder-list': {
|
|
124
|
+
const browse = picker()
|
|
125
|
+
if (browse?.kind !== 'browse') throw new Error('This DeepSeek Harness cannot list folders')
|
|
126
|
+
const listing = await browse.list(typeof body.path === 'string' && body.path !== '' ? body.path : undefined, signal)
|
|
127
|
+
return { path: listing.path, home: listing.home, crumbs: listing.crumbs, entries: listing.entries, truncated: listing.truncated }
|
|
128
|
+
}
|
|
129
|
+
default:
|
|
130
|
+
throw new Error(`Unknown endpoint ${endpoint}`)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
Object.assign(rt, { teamFolder, workDirOf, isFolder, cleanWorkDir, sessionsOfBot, approveFolder, folderEndpoint })
|
|
135
|
+
}
|