Skip to content

Commit a73746b

Browse files
committed
fix(open-resource): open resource tool to open existing files
1 parent cef321b commit a73746b

File tree

7 files changed

+115
-32
lines changed

7 files changed

+115
-32
lines changed

apps/sim/lib/copilot/orchestrator/tool-executor/index.ts

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import type {
6868
ListWorkspaceMcpServersParams,
6969
MoveFolderParams,
7070
MoveWorkflowParams,
71+
OpenResourceParams,
7172
RenameFolderParams,
7273
RenameWorkflowParams,
7374
RunBlockParams,
@@ -77,6 +78,7 @@ import type {
7778
SetGlobalWorkflowVariablesParams,
7879
UpdateWorkflowParams,
7980
UpdateWorkspaceMcpServerParams,
81+
ValidOpenResourceParams,
8082
} from './param-types'
8183
import { PLATFORM_ACTIONS_CONTENT } from './platform-actions'
8284
import { executeVfsGlob, executeVfsGrep, executeVfsList, executeVfsRead } from './vfs-tools'
@@ -106,6 +108,27 @@ import {
106108

107109
const logger = createLogger('CopilotToolExecutor')
108110

111+
function validateOpenResourceParams(
112+
params: OpenResourceParams
113+
): { success: true; params: ValidOpenResourceParams } | { success: false; error: string } {
114+
if (!params.type) {
115+
return { success: false, error: 'type is required' }
116+
}
117+
118+
if (!params.id) {
119+
return { success: false, error: `${params.type} resources require \`id\`` }
120+
}
121+
122+
return {
123+
success: true,
124+
params: {
125+
type: params.type,
126+
id: params.id,
127+
...(params.title ? { title: params.title } : {}),
128+
},
129+
}
130+
}
131+
109132
type ManageCustomToolOperation = 'add' | 'edit' | 'delete' | 'list'
110133

111134
interface ManageCustomToolSchema {
@@ -996,24 +1019,25 @@ const SIM_WORKFLOW_TOOL_HANDLERS: Record<
9961019
list: (p, c) => executeVfsList(p, c),
9971020

9981021
// Resource visibility
999-
open_resource: async (p) => {
1000-
const resourceType = p.type as string | undefined
1001-
const resourceId = p.id as string | undefined
1002-
if (!resourceType || !resourceId) {
1003-
return { success: false, error: 'type and id are required' }
1004-
}
1005-
const validTypes = new Set(['workflow', 'table', 'knowledgebase', 'file'])
1006-
if (!validTypes.has(resourceType)) {
1007-
return { success: false, error: `Invalid resource type: ${resourceType}` }
1022+
open_resource: async (p: OpenResourceParams) => {
1023+
const validated = validateOpenResourceParams(p)
1024+
if (!validated.success) {
1025+
return { success: false, error: validated.error }
10081026
}
1027+
1028+
const params = validated.params
1029+
const resourceType = params.type
1030+
const resourceId = params.id
1031+
const resourceTitle = params.title || resourceType
1032+
10091033
return {
10101034
success: true,
10111035
output: { message: `Opened ${resourceType} ${resourceId} for the user` },
10121036
resources: [
10131037
{
10141038
type: resourceType as 'workflow' | 'table' | 'knowledgebase' | 'file',
10151039
id: resourceId,
1016-
title: resourceType,
1040+
title: resourceTitle,
10171041
},
10181042
],
10191043
}

apps/sim/lib/copilot/orchestrator/tool-executor/integration-tools.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
1515
import { getTableById, queryRows } from '@/lib/table/service'
1616
import {
1717
downloadWorkspaceFile,
18+
findWorkspaceFileRecord,
1819
listWorkspaceFiles,
1920
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
2021
import { getWorkflowById } from '@/lib/workflows/utils'
@@ -178,9 +179,7 @@ export async function executeIntegrationToolDirect(
178179
logger.warn('Skipping non-text sandbox input file', { fileName, ext })
179180
continue
180181
}
181-
const record = allFiles.find(
182-
(f) => f.name === fileName || f.name.normalize('NFC') === fileName.normalize('NFC')
183-
)
182+
const record = findWorkspaceFileRecord(allFiles, filePath)
184183
if (!record) {
185184
logger.warn('Sandbox input file not found', { fileName })
186185
continue

apps/sim/lib/copilot/orchestrator/tool-executor/param-types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,17 @@ export interface UpdateWorkspaceMcpServerParams {
202202
export interface DeleteWorkspaceMcpServerParams {
203203
serverId: string
204204
}
205+
206+
export type OpenResourceType = 'workflow' | 'table' | 'knowledgebase' | 'file'
207+
208+
export interface OpenResourceParams {
209+
type?: OpenResourceType
210+
id?: string
211+
title?: string
212+
}
213+
214+
export interface ValidOpenResourceParams {
215+
type: OpenResourceType
216+
id: string
217+
title?: string
218+
}

apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
updateTagDefinition,
2929
} from '@/lib/knowledge/tags/service'
3030
import { StorageService } from '@/lib/uploads'
31-
import { listWorkspaceFiles } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
31+
import { resolveWorkspaceFileReference } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
3232
import { getQueryStrategy, handleVectorOnlySearch } from '@/app/api/knowledge/search/utils'
3333

3434
const logger = createLogger('KnowledgeBaseServerTool')
@@ -235,13 +235,8 @@ export const knowledgeBaseServerTool: BaseServerTool<KnowledgeBaseArgs, Knowledg
235235
}
236236
}
237237

238-
const match = args.filePath.match(/^files\/(.+)$/)
239-
const fileName = match ? match[1] : args.filePath
240238
const kbWorkspaceId: string = targetKb.workspaceId
241-
const files = await listWorkspaceFiles(kbWorkspaceId)
242-
const fileRecord = files.find(
243-
(f) => f.name === fileName || f.name.normalize('NFC') === fileName.normalize('NFC')
244-
)
239+
const fileRecord = await resolveWorkspaceFileReference(kbWorkspaceId, args.filePath)
245240

246241
if (!fileRecord) {
247242
return {

apps/sim/lib/copilot/tools/server/table/user-table.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
import type { ColumnDefinition, RowData, TableDefinition } from '@/lib/table/types'
2727
import {
2828
downloadWorkspaceFile,
29-
listWorkspaceFiles,
29+
resolveWorkspaceFileReference,
3030
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
3131

3232
const logger = createLogger('UserTableServerTool')
@@ -40,15 +40,10 @@ async function resolveWorkspaceFile(
4040
filePath: string,
4141
workspaceId: string
4242
): Promise<{ buffer: Buffer; name: string; type: string }> {
43-
const match = filePath.match(/^files\/(.+)$/)
44-
const fileName = match ? match[1] : filePath
45-
const files = await listWorkspaceFiles(workspaceId)
46-
const record = files.find(
47-
(f) => f.name === fileName || f.name.normalize('NFC') === fileName.normalize('NFC')
48-
)
43+
const record = await resolveWorkspaceFileReference(workspaceId, filePath)
4944
if (!record) {
5045
throw new Error(
51-
`File not found: "${fileName}". Use glob("files/*/meta.json") to list available files.`
46+
`File not found: "${filePath}". Use glob("files/*/meta.json") to list available files.`
5247
)
5348
}
5449
const buffer = await downloadWorkspaceFile(record)

apps/sim/lib/copilot/vfs/workspace-vfs.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ import {
5656
import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils'
5757
import { getKnowledgeBases } from '@/lib/knowledge/service'
5858
import { listTables } from '@/lib/table/service'
59-
import { listWorkspaceFiles } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
59+
import {
60+
findWorkspaceFileRecord,
61+
listWorkspaceFiles,
62+
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
6063
import { hasWorkflowChanged } from '@/lib/workflows/comparison'
6164
import { listCustomTools } from '@/lib/workflows/custom-tools/operations'
6265
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
@@ -397,9 +400,7 @@ export class WorkspaceVFS {
397400

398401
try {
399402
const files = await listWorkspaceFiles(this._workspaceId)
400-
const record = files.find(
401-
(f) => f.name === fileName || f.name.normalize('NFC') === fileName.normalize('NFC')
402-
)
403+
const record = findWorkspaceFileRecord(files, fileName)
403404
if (!record) return null
404405
return readFileRecord(record)
405406
} catch (err) {

apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,61 @@ export async function listWorkspaceFiles(
328328
}
329329
}
330330

331+
/**
332+
* Normalize a workspace file reference to its display name.
333+
* Supports raw names and VFS-style paths like `files/name`, `files/name/content`,
334+
* and `files/name/meta.json`.
335+
*/
336+
export function normalizeWorkspaceFileReference(fileReference: string): string {
337+
const trimmed = fileReference.trim().replace(/^\/+/, '')
338+
339+
if (trimmed.startsWith('files/')) {
340+
const withoutPrefix = trimmed.slice('files/'.length)
341+
if (withoutPrefix.endsWith('/meta.json')) {
342+
return withoutPrefix.slice(0, -'/meta.json'.length)
343+
}
344+
if (withoutPrefix.endsWith('/content')) {
345+
return withoutPrefix.slice(0, -'/content'.length)
346+
}
347+
return withoutPrefix
348+
}
349+
350+
return trimmed
351+
}
352+
353+
/**
354+
* Find a workspace file record in an existing list from either its id or a VFS/name reference.
355+
*/
356+
export function findWorkspaceFileRecord(
357+
files: WorkspaceFileRecord[],
358+
fileReference: string
359+
): WorkspaceFileRecord | null {
360+
const exactIdMatch = files.find((file) => file.id === fileReference)
361+
if (exactIdMatch) {
362+
return exactIdMatch
363+
}
364+
365+
const normalizedReference = normalizeWorkspaceFileReference(fileReference)
366+
return (
367+
files.find(
368+
(file) =>
369+
file.name === normalizedReference ||
370+
file.name.normalize('NFC') === normalizedReference.normalize('NFC')
371+
) ?? null
372+
)
373+
}
374+
375+
/**
376+
* Resolve a workspace file record from either its id or a VFS/name reference.
377+
*/
378+
export async function resolveWorkspaceFileReference(
379+
workspaceId: string,
380+
fileReference: string
381+
): Promise<WorkspaceFileRecord | null> {
382+
const files = await listWorkspaceFiles(workspaceId)
383+
return findWorkspaceFileRecord(files, fileReference)
384+
}
385+
331386
/**
332387
* Get a specific workspace file
333388
*/

0 commit comments

Comments
 (0)