@agent-native/core 0.45.1 → 0.47.1
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 +1 -0
- package/dist/agent/production-agent.d.ts +28 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +14 -7
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/skills.d.ts +2 -2
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +33 -0
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/components/LiveCursorOverlay.d.ts +46 -0
- package/dist/client/components/LiveCursorOverlay.d.ts.map +1 -0
- package/dist/client/components/LiveCursorOverlay.js +137 -0
- package/dist/client/components/LiveCursorOverlay.js.map +1 -0
- package/dist/client/components/PresenceBar.d.ts +11 -1
- package/dist/client/components/PresenceBar.d.ts.map +1 -1
- package/dist/client/components/PresenceBar.js +39 -7
- package/dist/client/components/PresenceBar.js.map +1 -1
- package/dist/client/components/RemoteSelectionRings.d.ts +43 -0
- package/dist/client/components/RemoteSelectionRings.d.ts.map +1 -0
- package/dist/client/components/RemoteSelectionRings.js +116 -0
- package/dist/client/components/RemoteSelectionRings.js.map +1 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +5 -0
- package/dist/client/index.js.map +1 -1
- package/dist/coding-tools/run-code.d.ts +40 -0
- package/dist/coding-tools/run-code.d.ts.map +1 -0
- package/dist/coding-tools/run-code.js +511 -0
- package/dist/coding-tools/run-code.js.map +1 -0
- package/dist/collab/awareness.d.ts +25 -0
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/awareness.js +42 -5
- package/dist/collab/awareness.js.map +1 -1
- package/dist/collab/client.d.ts +19 -1
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +362 -57
- package/dist/collab/client.js.map +1 -1
- package/dist/collab/follow-mode.d.ts +56 -0
- package/dist/collab/follow-mode.d.ts.map +1 -0
- package/dist/collab/follow-mode.js +54 -0
- package/dist/collab/follow-mode.js.map +1 -0
- package/dist/collab/index.d.ts +3 -1
- package/dist/collab/index.d.ts.map +1 -1
- package/dist/collab/index.js +5 -1
- package/dist/collab/index.js.map +1 -1
- package/dist/collab/presence.d.ts +56 -0
- package/dist/collab/presence.d.ts.map +1 -0
- package/dist/collab/presence.js +98 -0
- package/dist/collab/presence.js.map +1 -0
- package/dist/collab/routes.d.ts.map +1 -1
- package/dist/collab/routes.js +33 -6
- package/dist/collab/routes.js.map +1 -1
- package/dist/collab/struct-routes.d.ts.map +1 -1
- package/dist/collab/struct-routes.js +24 -4
- package/dist/collab/struct-routes.js.map +1 -1
- package/dist/collab/ydoc-manager.d.ts +13 -0
- package/dist/collab/ydoc-manager.d.ts.map +1 -1
- package/dist/collab/ydoc-manager.js +51 -15
- package/dist/collab/ydoc-manager.js.map +1 -1
- package/dist/extensions/fetch-tool.d.ts.map +1 -1
- package/dist/extensions/fetch-tool.js +62 -7
- package/dist/extensions/fetch-tool.js.map +1 -1
- package/dist/extensions/web-search-tool.d.ts +41 -0
- package/dist/extensions/web-search-tool.d.ts.map +1 -0
- package/dist/extensions/web-search-tool.js +200 -0
- package/dist/extensions/web-search-tool.js.map +1 -0
- package/dist/provider-api/custom-registry.d.ts +92 -0
- package/dist/provider-api/custom-registry.d.ts.map +1 -0
- package/dist/provider-api/custom-registry.js +289 -0
- package/dist/provider-api/custom-registry.js.map +1 -0
- package/dist/provider-api/index.d.ts +80 -44
- package/dist/provider-api/index.d.ts.map +1 -1
- package/dist/provider-api/index.js +569 -18
- package/dist/provider-api/index.js.map +1 -1
- package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
- package/dist/secrets/register-framework-secrets.js +36 -3
- package/dist/secrets/register-framework-secrets.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts +36 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +119 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/collab-plugin.d.ts +6 -0
- package/dist/server/collab-plugin.d.ts.map +1 -1
- package/dist/server/collab-plugin.js +105 -5
- package/dist/server/collab-plugin.js.map +1 -1
- package/dist/server/poll-events.d.ts +5 -0
- package/dist/server/poll-events.d.ts.map +1 -1
- package/dist/server/poll-events.js +27 -4
- package/dist/server/poll-events.js.map +1 -1
- package/dist/templates/default/.agents/skills/real-time-collab/SKILL.md +185 -37
- package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +12 -2
- package/dist/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +185 -37
- package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +12 -2
- package/dist/workspace-files/index.d.ts +4 -0
- package/dist/workspace-files/index.d.ts.map +1 -0
- package/dist/workspace-files/index.js +4 -0
- package/dist/workspace-files/index.js.map +1 -0
- package/dist/workspace-files/schema.d.ts +195 -0
- package/dist/workspace-files/schema.d.ts.map +1 -0
- package/dist/workspace-files/schema.js +48 -0
- package/dist/workspace-files/schema.js.map +1 -0
- package/dist/workspace-files/store.d.ts +89 -0
- package/dist/workspace-files/store.d.ts.map +1 -0
- package/dist/workspace-files/store.js +298 -0
- package/dist/workspace-files/store.js.map +1 -0
- package/dist/workspace-files/tool.d.ts +15 -0
- package/dist/workspace-files/tool.d.ts.map +1 -0
- package/dist/workspace-files/tool.js +226 -0
- package/dist/workspace-files/tool.js.map +1 -0
- package/docs/content/real-time-collaboration.md +481 -97
- package/package.json +2 -1
- package/src/templates/default/.agents/skills/real-time-collab/SKILL.md +185 -37
- package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +12 -2
- package/src/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +185 -37
- package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +12 -2
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace-files store.
|
|
3
|
+
*
|
|
4
|
+
* Provides read/write/append/list/delete/grep operations over the
|
|
5
|
+
* `workspace_files` table. All writes enforce per-file (2 MB) and per-scope
|
|
6
|
+
* (200 MB) caps. The table is lazily migrated on first use.
|
|
7
|
+
*/
|
|
8
|
+
/** Max content size per file (bytes). */
|
|
9
|
+
export declare const MAX_FILE_BYTES: number;
|
|
10
|
+
/** Max total content size across all files in one scope (bytes). */
|
|
11
|
+
export declare const MAX_SCOPE_BYTES: number;
|
|
12
|
+
/** Max content size when saving via saveToFile from provider-api / fetch tool (bytes). */
|
|
13
|
+
export declare const SAVE_TO_FILE_MAX_BYTES: number;
|
|
14
|
+
export interface WorkspaceFilesScope {
|
|
15
|
+
scope: "user" | "org";
|
|
16
|
+
scopeId: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Validate a workspace file path.
|
|
20
|
+
* - Non-empty, no leading slash, no ".." components, no null bytes.
|
|
21
|
+
*/
|
|
22
|
+
export declare function validatePath(path: string): string | null;
|
|
23
|
+
export interface WorkspaceFile {
|
|
24
|
+
id: string;
|
|
25
|
+
scope: string;
|
|
26
|
+
scopeId: string;
|
|
27
|
+
path: string;
|
|
28
|
+
content: string;
|
|
29
|
+
contentType: string;
|
|
30
|
+
sizeBytes: number;
|
|
31
|
+
createdAt: string;
|
|
32
|
+
updatedAt: string;
|
|
33
|
+
}
|
|
34
|
+
export interface WorkspaceFileMeta {
|
|
35
|
+
id: string;
|
|
36
|
+
path: string;
|
|
37
|
+
contentType: string;
|
|
38
|
+
sizeBytes: number;
|
|
39
|
+
createdAt: string;
|
|
40
|
+
updatedAt: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Write (create or overwrite) a workspace file.
|
|
44
|
+
* Enforces per-file (2 MB default; `saveToFile` callers may raise it up to
|
|
45
|
+
* 20 MB via `opts.maxFileBytes`) and per-scope (200 MB) caps.
|
|
46
|
+
*/
|
|
47
|
+
export declare function writeWorkspaceFile(scope: WorkspaceFilesScope, path: string, content: string, contentType?: string, opts?: {
|
|
48
|
+
maxFileBytes?: number;
|
|
49
|
+
}): Promise<WorkspaceFileMeta>;
|
|
50
|
+
/**
|
|
51
|
+
* Append text to an existing workspace file, or create it if it doesn't exist.
|
|
52
|
+
*/
|
|
53
|
+
export declare function appendWorkspaceFile(scope: WorkspaceFilesScope, path: string, text: string, contentType?: string): Promise<WorkspaceFileMeta>;
|
|
54
|
+
/**
|
|
55
|
+
* Read a workspace file's content (with optional offset and maxChars for paging).
|
|
56
|
+
* Returns null if the file doesn't exist.
|
|
57
|
+
*/
|
|
58
|
+
export declare function readWorkspaceFile(scope: WorkspaceFilesScope, path: string, opts?: {
|
|
59
|
+
offset?: number;
|
|
60
|
+
maxChars?: number;
|
|
61
|
+
}): Promise<WorkspaceFile | null>;
|
|
62
|
+
/**
|
|
63
|
+
* Get file metadata without loading content.
|
|
64
|
+
*/
|
|
65
|
+
export declare function getWorkspaceFileMeta(scope: WorkspaceFilesScope, path: string): Promise<WorkspaceFileMeta | null>;
|
|
66
|
+
/**
|
|
67
|
+
* List workspace files, optionally filtered by path prefix.
|
|
68
|
+
* Returns metadata only (no content).
|
|
69
|
+
*/
|
|
70
|
+
export declare function listWorkspaceFiles(scope: WorkspaceFilesScope, prefix?: string): Promise<WorkspaceFileMeta[]>;
|
|
71
|
+
/**
|
|
72
|
+
* Delete a workspace file. Returns true if deleted, false if not found.
|
|
73
|
+
*/
|
|
74
|
+
export declare function deleteWorkspaceFile(scope: WorkspaceFilesScope, path: string): Promise<boolean>;
|
|
75
|
+
/**
|
|
76
|
+
* Search file contents for a substring or regex pattern.
|
|
77
|
+
* Returns matching lines with path context.
|
|
78
|
+
*/
|
|
79
|
+
export declare function grepWorkspaceFiles(scope: WorkspaceFilesScope, pattern: string, opts?: {
|
|
80
|
+
pathPrefix?: string;
|
|
81
|
+
useRegex?: boolean;
|
|
82
|
+
maxMatchesPerFile?: number;
|
|
83
|
+
maxFiles?: number;
|
|
84
|
+
}): Promise<Array<{
|
|
85
|
+
path: string;
|
|
86
|
+
lineNumber: number;
|
|
87
|
+
line: string;
|
|
88
|
+
}>>;
|
|
89
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/workspace-files/store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,yCAAyC;AACzC,eAAO,MAAM,cAAc,QAAkB,CAAC;AAE9C,oEAAoE;AACpE,eAAO,MAAM,eAAe,QAAoB,CAAC;AAEjD,0FAA0F;AAC1F,eAAO,MAAM,sBAAsB,QAAmB,CAAC;AAoBvD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWxD;AAMD,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAMD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,mBAAmB,EAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,WAAW,SAAe,EAC1B,IAAI,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/B,OAAO,CAAC,iBAAiB,CAAC,CA4E5B;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,mBAAmB,EAC1B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,WAAW,SAAe,GACzB,OAAO,CAAC,iBAAiB,CAAC,CAQ5B;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,mBAAmB,EAC1B,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5C,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAkC/B;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,mBAAmB,EAC1B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAmBnC;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,mBAAmB,EAC1B,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA+B9B;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,mBAAmB,EAC1B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,OAAO,CAAC,CAWlB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;IACL,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GACA,OAAO,CAAC,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CA+BpE"}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace-files store.
|
|
3
|
+
*
|
|
4
|
+
* Provides read/write/append/list/delete/grep operations over the
|
|
5
|
+
* `workspace_files` table. All writes enforce per-file (2 MB) and per-scope
|
|
6
|
+
* (200 MB) caps. The table is lazily migrated on first use.
|
|
7
|
+
*/
|
|
8
|
+
import crypto from "node:crypto";
|
|
9
|
+
import { getDbExec } from "../db/client.js";
|
|
10
|
+
import { WORKSPACE_FILES_CREATE_SQL, WORKSPACE_FILES_INDEX_SQL, } from "./schema.js";
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Constants
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
/** Max content size per file (bytes). */
|
|
15
|
+
export const MAX_FILE_BYTES = 2 * 1024 * 1024; // 2 MB
|
|
16
|
+
/** Max total content size across all files in one scope (bytes). */
|
|
17
|
+
export const MAX_SCOPE_BYTES = 200 * 1024 * 1024; // 200 MB
|
|
18
|
+
/** Max content size when saving via saveToFile from provider-api / fetch tool (bytes). */
|
|
19
|
+
export const SAVE_TO_FILE_MAX_BYTES = 20 * 1024 * 1024; // 20 MB
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Lazy table init
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
let _tableReady = false;
|
|
24
|
+
async function ensureTable() {
|
|
25
|
+
if (_tableReady)
|
|
26
|
+
return;
|
|
27
|
+
const db = getDbExec();
|
|
28
|
+
await db.execute(WORKSPACE_FILES_CREATE_SQL);
|
|
29
|
+
await db.execute(WORKSPACE_FILES_INDEX_SQL);
|
|
30
|
+
_tableReady = true;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Validate a workspace file path.
|
|
34
|
+
* - Non-empty, no leading slash, no ".." components, no null bytes.
|
|
35
|
+
*/
|
|
36
|
+
export function validatePath(path) {
|
|
37
|
+
if (!path || typeof path !== "string")
|
|
38
|
+
return "path is required";
|
|
39
|
+
if (path.startsWith("/"))
|
|
40
|
+
return 'path must not start with "/"';
|
|
41
|
+
if (path.includes("\0"))
|
|
42
|
+
return "path must not contain null bytes";
|
|
43
|
+
const parts = path.split("/");
|
|
44
|
+
for (const part of parts) {
|
|
45
|
+
if (part === "..")
|
|
46
|
+
return 'path must not contain ".." components';
|
|
47
|
+
if (part === "")
|
|
48
|
+
return 'path must not contain empty segments ("//" or trailing "/")';
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Store operations
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
/**
|
|
56
|
+
* Write (create or overwrite) a workspace file.
|
|
57
|
+
* Enforces per-file (2 MB default; `saveToFile` callers may raise it up to
|
|
58
|
+
* 20 MB via `opts.maxFileBytes`) and per-scope (200 MB) caps.
|
|
59
|
+
*/
|
|
60
|
+
export async function writeWorkspaceFile(scope, path, content, contentType = "text/plain", opts) {
|
|
61
|
+
const pathErr = validatePath(path);
|
|
62
|
+
if (pathErr)
|
|
63
|
+
throw new Error(`Invalid path: ${pathErr}`);
|
|
64
|
+
const maxFileBytes = Math.min(opts?.maxFileBytes ?? MAX_FILE_BYTES, SAVE_TO_FILE_MAX_BYTES);
|
|
65
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
66
|
+
if (bytes > maxFileBytes) {
|
|
67
|
+
throw new Error(`File "${path}" would be ${(bytes / 1024 / 1024).toFixed(2)} MB, which exceeds the ${(maxFileBytes / 1024 / 1024).toFixed(0)} MB per-file limit.`);
|
|
68
|
+
}
|
|
69
|
+
await ensureTable();
|
|
70
|
+
const db = getDbExec();
|
|
71
|
+
// Check scope total (excluding current file's existing bytes).
|
|
72
|
+
const existing = await getWorkspaceFileMeta(scope, path);
|
|
73
|
+
const existingBytes = existing?.sizeBytes ?? 0;
|
|
74
|
+
const scopeTotal = await getScopeTotalBytes(scope);
|
|
75
|
+
const newTotal = scopeTotal - existingBytes + bytes;
|
|
76
|
+
if (newTotal > MAX_SCOPE_BYTES) {
|
|
77
|
+
throw new Error(`Writing "${path}" would bring the workspace total to ${(newTotal / 1024 / 1024).toFixed(1)} MB, exceeding the 200 MB limit.`);
|
|
78
|
+
}
|
|
79
|
+
const now = new Date().toISOString();
|
|
80
|
+
if (existing) {
|
|
81
|
+
await db.execute({
|
|
82
|
+
sql: `UPDATE workspace_files SET content = ?, content_type = ?, size_bytes = ?, updated_at = ? WHERE scope = ? AND scope_id = ? AND path = ?`,
|
|
83
|
+
args: [
|
|
84
|
+
content,
|
|
85
|
+
contentType,
|
|
86
|
+
bytes,
|
|
87
|
+
now,
|
|
88
|
+
scope.scope,
|
|
89
|
+
scope.scopeId,
|
|
90
|
+
path,
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
...existing,
|
|
95
|
+
content: undefined,
|
|
96
|
+
contentType,
|
|
97
|
+
sizeBytes: bytes,
|
|
98
|
+
updatedAt: now,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const id = crypto.randomUUID();
|
|
102
|
+
await db.execute({
|
|
103
|
+
sql: `INSERT INTO workspace_files (id, scope, scope_id, path, content, content_type, size_bytes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
104
|
+
args: [
|
|
105
|
+
id,
|
|
106
|
+
scope.scope,
|
|
107
|
+
scope.scopeId,
|
|
108
|
+
path,
|
|
109
|
+
content,
|
|
110
|
+
contentType,
|
|
111
|
+
bytes,
|
|
112
|
+
now,
|
|
113
|
+
now,
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
id,
|
|
118
|
+
path,
|
|
119
|
+
contentType,
|
|
120
|
+
sizeBytes: bytes,
|
|
121
|
+
createdAt: now,
|
|
122
|
+
updatedAt: now,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Append text to an existing workspace file, or create it if it doesn't exist.
|
|
127
|
+
*/
|
|
128
|
+
export async function appendWorkspaceFile(scope, path, text, contentType = "text/plain") {
|
|
129
|
+
const pathErr = validatePath(path);
|
|
130
|
+
if (pathErr)
|
|
131
|
+
throw new Error(`Invalid path: ${pathErr}`);
|
|
132
|
+
await ensureTable();
|
|
133
|
+
const existing = await readWorkspaceFile(scope, path);
|
|
134
|
+
const newContent = existing ? existing.content + text : text;
|
|
135
|
+
return writeWorkspaceFile(scope, path, newContent, contentType);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Read a workspace file's content (with optional offset and maxChars for paging).
|
|
139
|
+
* Returns null if the file doesn't exist.
|
|
140
|
+
*/
|
|
141
|
+
export async function readWorkspaceFile(scope, path, opts) {
|
|
142
|
+
const pathErr = validatePath(path);
|
|
143
|
+
if (pathErr)
|
|
144
|
+
throw new Error(`Invalid path: ${pathErr}`);
|
|
145
|
+
await ensureTable();
|
|
146
|
+
const db = getDbExec();
|
|
147
|
+
const result = await db.execute({
|
|
148
|
+
sql: `SELECT id, scope, scope_id, path, content, content_type, size_bytes, created_at, updated_at FROM workspace_files WHERE scope = ? AND scope_id = ? AND path = ?`,
|
|
149
|
+
args: [scope.scope, scope.scopeId, path],
|
|
150
|
+
});
|
|
151
|
+
const row = result.rows[0];
|
|
152
|
+
if (!row)
|
|
153
|
+
return null;
|
|
154
|
+
let content = String(row[4] ?? "");
|
|
155
|
+
if (opts?.offset || opts?.maxChars) {
|
|
156
|
+
const off = opts.offset ?? 0;
|
|
157
|
+
content = content.slice(off, opts.maxChars !== undefined ? off + opts.maxChars : undefined);
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
id: String(row[0]),
|
|
161
|
+
scope: String(row[1]),
|
|
162
|
+
scopeId: String(row[2]),
|
|
163
|
+
path: String(row[3]),
|
|
164
|
+
content,
|
|
165
|
+
contentType: String(row[5] ?? "text/plain"),
|
|
166
|
+
sizeBytes: Number(row[6] ?? 0),
|
|
167
|
+
createdAt: String(row[7]),
|
|
168
|
+
updatedAt: String(row[8]),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get file metadata without loading content.
|
|
173
|
+
*/
|
|
174
|
+
export async function getWorkspaceFileMeta(scope, path) {
|
|
175
|
+
await ensureTable();
|
|
176
|
+
const db = getDbExec();
|
|
177
|
+
const result = await db.execute({
|
|
178
|
+
sql: `SELECT id, path, content_type, size_bytes, created_at, updated_at FROM workspace_files WHERE scope = ? AND scope_id = ? AND path = ?`,
|
|
179
|
+
args: [scope.scope, scope.scopeId, path],
|
|
180
|
+
});
|
|
181
|
+
const row = result.rows[0];
|
|
182
|
+
if (!row)
|
|
183
|
+
return null;
|
|
184
|
+
return {
|
|
185
|
+
id: String(row[0]),
|
|
186
|
+
path: String(row[1]),
|
|
187
|
+
contentType: String(row[2] ?? "text/plain"),
|
|
188
|
+
sizeBytes: Number(row[3] ?? 0),
|
|
189
|
+
createdAt: String(row[4]),
|
|
190
|
+
updatedAt: String(row[5]),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* List workspace files, optionally filtered by path prefix.
|
|
195
|
+
* Returns metadata only (no content).
|
|
196
|
+
*/
|
|
197
|
+
export async function listWorkspaceFiles(scope, prefix) {
|
|
198
|
+
await ensureTable();
|
|
199
|
+
const db = getDbExec();
|
|
200
|
+
if (prefix) {
|
|
201
|
+
// Allow a trailing slash on list prefixes, but reject traversal and
|
|
202
|
+
// other invalid path shapes before they reach the LIKE pattern.
|
|
203
|
+
const normalizedPrefix = prefix.endsWith("/")
|
|
204
|
+
? prefix.slice(0, -1)
|
|
205
|
+
: prefix;
|
|
206
|
+
const pathErr = validatePath(normalizedPrefix);
|
|
207
|
+
if (pathErr) {
|
|
208
|
+
throw new Error(pathErr);
|
|
209
|
+
}
|
|
210
|
+
const result = await db.execute({
|
|
211
|
+
sql: `SELECT id, path, content_type, size_bytes, created_at, updated_at FROM workspace_files WHERE scope = ? AND scope_id = ? AND (path = ? OR path LIKE ? ESCAPE '\\') ORDER BY path ASC`,
|
|
212
|
+
args: [
|
|
213
|
+
scope.scope,
|
|
214
|
+
scope.scopeId,
|
|
215
|
+
normalizedPrefix,
|
|
216
|
+
`${normalizedPrefix.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_")}/%`,
|
|
217
|
+
],
|
|
218
|
+
});
|
|
219
|
+
return result.rows.map(rowToMeta);
|
|
220
|
+
}
|
|
221
|
+
const result = await db.execute({
|
|
222
|
+
sql: `SELECT id, path, content_type, size_bytes, created_at, updated_at FROM workspace_files WHERE scope = ? AND scope_id = ? ORDER BY path ASC`,
|
|
223
|
+
args: [scope.scope, scope.scopeId],
|
|
224
|
+
});
|
|
225
|
+
return result.rows.map(rowToMeta);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Delete a workspace file. Returns true if deleted, false if not found.
|
|
229
|
+
*/
|
|
230
|
+
export async function deleteWorkspaceFile(scope, path) {
|
|
231
|
+
const pathErr = validatePath(path);
|
|
232
|
+
if (pathErr)
|
|
233
|
+
throw new Error(`Invalid path: ${pathErr}`);
|
|
234
|
+
await ensureTable();
|
|
235
|
+
const db = getDbExec();
|
|
236
|
+
const result = await db.execute({
|
|
237
|
+
sql: `DELETE FROM workspace_files WHERE scope = ? AND scope_id = ? AND path = ?`,
|
|
238
|
+
args: [scope.scope, scope.scopeId, path],
|
|
239
|
+
});
|
|
240
|
+
return result.rowsAffected > 0;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Search file contents for a substring or regex pattern.
|
|
244
|
+
* Returns matching lines with path context.
|
|
245
|
+
*/
|
|
246
|
+
export async function grepWorkspaceFiles(scope, pattern, opts) {
|
|
247
|
+
const files = await listWorkspaceFiles(scope, opts?.pathPrefix);
|
|
248
|
+
const limited = files.slice(0, opts?.maxFiles ?? 50);
|
|
249
|
+
let regex;
|
|
250
|
+
try {
|
|
251
|
+
regex = opts?.useRegex
|
|
252
|
+
? new RegExp(pattern, "i")
|
|
253
|
+
: new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
throw new Error(`Invalid regex pattern: ${pattern}`);
|
|
257
|
+
}
|
|
258
|
+
const results = [];
|
|
259
|
+
const maxPerFile = opts?.maxMatchesPerFile ?? 20;
|
|
260
|
+
for (const meta of limited) {
|
|
261
|
+
const file = await readWorkspaceFile(scope, meta.path);
|
|
262
|
+
if (!file)
|
|
263
|
+
continue;
|
|
264
|
+
const lines = file.content.split("\n");
|
|
265
|
+
let matchCount = 0;
|
|
266
|
+
for (let i = 0; i < lines.length; i++) {
|
|
267
|
+
if (regex.test(lines[i])) {
|
|
268
|
+
results.push({ path: meta.path, lineNumber: i + 1, line: lines[i] });
|
|
269
|
+
matchCount++;
|
|
270
|
+
if (matchCount >= maxPerFile)
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return results;
|
|
276
|
+
}
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// Internal helpers
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
async function getScopeTotalBytes(scope) {
|
|
281
|
+
const db = getDbExec();
|
|
282
|
+
const result = await db.execute({
|
|
283
|
+
sql: `SELECT COALESCE(SUM(size_bytes), 0) as total FROM workspace_files WHERE scope = ? AND scope_id = ?`,
|
|
284
|
+
args: [scope.scope, scope.scopeId],
|
|
285
|
+
});
|
|
286
|
+
return Number(result.rows[0]?.[0] ?? 0);
|
|
287
|
+
}
|
|
288
|
+
function rowToMeta(row) {
|
|
289
|
+
return {
|
|
290
|
+
id: String(row[0]),
|
|
291
|
+
path: String(row[1]),
|
|
292
|
+
contentType: String(row[2] ?? "text/plain"),
|
|
293
|
+
sizeBytes: Number(row[3] ?? 0),
|
|
294
|
+
createdAt: String(row[4]),
|
|
295
|
+
updatedAt: String(row[5]),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/workspace-files/store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Workspace-files store.\n *\n * Provides read/write/append/list/delete/grep operations over the\n * `workspace_files` table. All writes enforce per-file (2 MB) and per-scope\n * (200 MB) caps. The table is lazily migrated on first use.\n */\n\nimport crypto from \"node:crypto\";\nimport { getDbExec } from \"../db/client.js\";\nimport {\n WORKSPACE_FILES_CREATE_SQL,\n WORKSPACE_FILES_INDEX_SQL,\n} from \"./schema.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Max content size per file (bytes). */\nexport const MAX_FILE_BYTES = 2 * 1024 * 1024; // 2 MB\n\n/** Max total content size across all files in one scope (bytes). */\nexport const MAX_SCOPE_BYTES = 200 * 1024 * 1024; // 200 MB\n\n/** Max content size when saving via saveToFile from provider-api / fetch tool (bytes). */\nexport const SAVE_TO_FILE_MAX_BYTES = 20 * 1024 * 1024; // 20 MB\n\n// ---------------------------------------------------------------------------\n// Lazy table init\n// ---------------------------------------------------------------------------\n\nlet _tableReady = false;\n\nasync function ensureTable(): Promise<void> {\n if (_tableReady) return;\n const db = getDbExec();\n await db.execute(WORKSPACE_FILES_CREATE_SQL);\n await db.execute(WORKSPACE_FILES_INDEX_SQL);\n _tableReady = true;\n}\n\n// ---------------------------------------------------------------------------\n// Scope helpers\n// ---------------------------------------------------------------------------\n\nexport interface WorkspaceFilesScope {\n scope: \"user\" | \"org\";\n scopeId: string;\n}\n\n/**\n * Validate a workspace file path.\n * - Non-empty, no leading slash, no \"..\" components, no null bytes.\n */\nexport function validatePath(path: string): string | null {\n if (!path || typeof path !== \"string\") return \"path is required\";\n if (path.startsWith(\"/\")) return 'path must not start with \"/\"';\n if (path.includes(\"\\0\")) return \"path must not contain null bytes\";\n const parts = path.split(\"/\");\n for (const part of parts) {\n if (part === \"..\") return 'path must not contain \"..\" components';\n if (part === \"\")\n return 'path must not contain empty segments (\"//\" or trailing \"/\")';\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface WorkspaceFile {\n id: string;\n scope: string;\n scopeId: string;\n path: string;\n content: string;\n contentType: string;\n sizeBytes: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface WorkspaceFileMeta {\n id: string;\n path: string;\n contentType: string;\n sizeBytes: number;\n createdAt: string;\n updatedAt: string;\n}\n\n// ---------------------------------------------------------------------------\n// Store operations\n// ---------------------------------------------------------------------------\n\n/**\n * Write (create or overwrite) a workspace file.\n * Enforces per-file (2 MB default; `saveToFile` callers may raise it up to\n * 20 MB via `opts.maxFileBytes`) and per-scope (200 MB) caps.\n */\nexport async function writeWorkspaceFile(\n scope: WorkspaceFilesScope,\n path: string,\n content: string,\n contentType = \"text/plain\",\n opts?: { maxFileBytes?: number },\n): Promise<WorkspaceFileMeta> {\n const pathErr = validatePath(path);\n if (pathErr) throw new Error(`Invalid path: ${pathErr}`);\n\n const maxFileBytes = Math.min(\n opts?.maxFileBytes ?? MAX_FILE_BYTES,\n SAVE_TO_FILE_MAX_BYTES,\n );\n const bytes = Buffer.byteLength(content, \"utf8\");\n if (bytes > maxFileBytes) {\n throw new Error(\n `File \"${path}\" would be ${(bytes / 1024 / 1024).toFixed(2)} MB, which exceeds the ${(maxFileBytes / 1024 / 1024).toFixed(0)} MB per-file limit.`,\n );\n }\n\n await ensureTable();\n const db = getDbExec();\n\n // Check scope total (excluding current file's existing bytes).\n const existing = await getWorkspaceFileMeta(scope, path);\n const existingBytes = existing?.sizeBytes ?? 0;\n const scopeTotal = await getScopeTotalBytes(scope);\n const newTotal = scopeTotal - existingBytes + bytes;\n if (newTotal > MAX_SCOPE_BYTES) {\n throw new Error(\n `Writing \"${path}\" would bring the workspace total to ${(newTotal / 1024 / 1024).toFixed(1)} MB, exceeding the 200 MB limit.`,\n );\n }\n\n const now = new Date().toISOString();\n\n if (existing) {\n await db.execute({\n sql: `UPDATE workspace_files SET content = ?, content_type = ?, size_bytes = ?, updated_at = ? WHERE scope = ? AND scope_id = ? AND path = ?`,\n args: [\n content,\n contentType,\n bytes,\n now,\n scope.scope,\n scope.scopeId,\n path,\n ],\n });\n return {\n ...existing,\n content: undefined as any,\n contentType,\n sizeBytes: bytes,\n updatedAt: now,\n } as unknown as WorkspaceFileMeta;\n }\n\n const id = crypto.randomUUID();\n await db.execute({\n sql: `INSERT INTO workspace_files (id, scope, scope_id, path, content, content_type, size_bytes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n args: [\n id,\n scope.scope,\n scope.scopeId,\n path,\n content,\n contentType,\n bytes,\n now,\n now,\n ],\n });\n return {\n id,\n path,\n contentType,\n sizeBytes: bytes,\n createdAt: now,\n updatedAt: now,\n };\n}\n\n/**\n * Append text to an existing workspace file, or create it if it doesn't exist.\n */\nexport async function appendWorkspaceFile(\n scope: WorkspaceFilesScope,\n path: string,\n text: string,\n contentType = \"text/plain\",\n): Promise<WorkspaceFileMeta> {\n const pathErr = validatePath(path);\n if (pathErr) throw new Error(`Invalid path: ${pathErr}`);\n\n await ensureTable();\n const existing = await readWorkspaceFile(scope, path);\n const newContent = existing ? existing.content + text : text;\n return writeWorkspaceFile(scope, path, newContent, contentType);\n}\n\n/**\n * Read a workspace file's content (with optional offset and maxChars for paging).\n * Returns null if the file doesn't exist.\n */\nexport async function readWorkspaceFile(\n scope: WorkspaceFilesScope,\n path: string,\n opts?: { offset?: number; maxChars?: number },\n): Promise<WorkspaceFile | null> {\n const pathErr = validatePath(path);\n if (pathErr) throw new Error(`Invalid path: ${pathErr}`);\n\n await ensureTable();\n const db = getDbExec();\n const result = await db.execute({\n sql: `SELECT id, scope, scope_id, path, content, content_type, size_bytes, created_at, updated_at FROM workspace_files WHERE scope = ? AND scope_id = ? AND path = ?`,\n args: [scope.scope, scope.scopeId, path],\n });\n\n const row = result.rows[0];\n if (!row) return null;\n\n let content = String(row[4] ?? \"\");\n if (opts?.offset || opts?.maxChars) {\n const off = opts.offset ?? 0;\n content = content.slice(\n off,\n opts.maxChars !== undefined ? off + opts.maxChars : undefined,\n );\n }\n\n return {\n id: String(row[0]),\n scope: String(row[1]),\n scopeId: String(row[2]),\n path: String(row[3]),\n content,\n contentType: String(row[5] ?? \"text/plain\"),\n sizeBytes: Number(row[6] ?? 0),\n createdAt: String(row[7]),\n updatedAt: String(row[8]),\n };\n}\n\n/**\n * Get file metadata without loading content.\n */\nexport async function getWorkspaceFileMeta(\n scope: WorkspaceFilesScope,\n path: string,\n): Promise<WorkspaceFileMeta | null> {\n await ensureTable();\n const db = getDbExec();\n const result = await db.execute({\n sql: `SELECT id, path, content_type, size_bytes, created_at, updated_at FROM workspace_files WHERE scope = ? AND scope_id = ? AND path = ?`,\n args: [scope.scope, scope.scopeId, path],\n });\n\n const row = result.rows[0];\n if (!row) return null;\n\n return {\n id: String(row[0]),\n path: String(row[1]),\n contentType: String(row[2] ?? \"text/plain\"),\n sizeBytes: Number(row[3] ?? 0),\n createdAt: String(row[4]),\n updatedAt: String(row[5]),\n };\n}\n\n/**\n * List workspace files, optionally filtered by path prefix.\n * Returns metadata only (no content).\n */\nexport async function listWorkspaceFiles(\n scope: WorkspaceFilesScope,\n prefix?: string,\n): Promise<WorkspaceFileMeta[]> {\n await ensureTable();\n const db = getDbExec();\n\n if (prefix) {\n // Allow a trailing slash on list prefixes, but reject traversal and\n // other invalid path shapes before they reach the LIKE pattern.\n const normalizedPrefix = prefix.endsWith(\"/\")\n ? prefix.slice(0, -1)\n : prefix;\n const pathErr = validatePath(normalizedPrefix);\n if (pathErr) {\n throw new Error(pathErr);\n }\n const result = await db.execute({\n sql: `SELECT id, path, content_type, size_bytes, created_at, updated_at FROM workspace_files WHERE scope = ? AND scope_id = ? AND (path = ? OR path LIKE ? ESCAPE '\\\\') ORDER BY path ASC`,\n args: [\n scope.scope,\n scope.scopeId,\n normalizedPrefix,\n `${normalizedPrefix.replace(/\\\\/g, \"\\\\\\\\\").replace(/%/g, \"\\\\%\").replace(/_/g, \"\\\\_\")}/%`,\n ],\n });\n return result.rows.map(rowToMeta);\n }\n\n const result = await db.execute({\n sql: `SELECT id, path, content_type, size_bytes, created_at, updated_at FROM workspace_files WHERE scope = ? AND scope_id = ? ORDER BY path ASC`,\n args: [scope.scope, scope.scopeId],\n });\n return result.rows.map(rowToMeta);\n}\n\n/**\n * Delete a workspace file. Returns true if deleted, false if not found.\n */\nexport async function deleteWorkspaceFile(\n scope: WorkspaceFilesScope,\n path: string,\n): Promise<boolean> {\n const pathErr = validatePath(path);\n if (pathErr) throw new Error(`Invalid path: ${pathErr}`);\n\n await ensureTable();\n const db = getDbExec();\n const result = await db.execute({\n sql: `DELETE FROM workspace_files WHERE scope = ? AND scope_id = ? AND path = ?`,\n args: [scope.scope, scope.scopeId, path],\n });\n return result.rowsAffected > 0;\n}\n\n/**\n * Search file contents for a substring or regex pattern.\n * Returns matching lines with path context.\n */\nexport async function grepWorkspaceFiles(\n scope: WorkspaceFilesScope,\n pattern: string,\n opts?: {\n pathPrefix?: string;\n useRegex?: boolean;\n maxMatchesPerFile?: number;\n maxFiles?: number;\n },\n): Promise<Array<{ path: string; lineNumber: number; line: string }>> {\n const files = await listWorkspaceFiles(scope, opts?.pathPrefix);\n const limited = files.slice(0, opts?.maxFiles ?? 50);\n\n let regex: RegExp;\n try {\n regex = opts?.useRegex\n ? new RegExp(pattern, \"i\")\n : new RegExp(pattern.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"), \"i\");\n } catch {\n throw new Error(`Invalid regex pattern: ${pattern}`);\n }\n\n const results: Array<{ path: string; lineNumber: number; line: string }> = [];\n const maxPerFile = opts?.maxMatchesPerFile ?? 20;\n\n for (const meta of limited) {\n const file = await readWorkspaceFile(scope, meta.path);\n if (!file) continue;\n const lines = file.content.split(\"\\n\");\n let matchCount = 0;\n for (let i = 0; i < lines.length; i++) {\n if (regex.test(lines[i])) {\n results.push({ path: meta.path, lineNumber: i + 1, line: lines[i] });\n matchCount++;\n if (matchCount >= maxPerFile) break;\n }\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nasync function getScopeTotalBytes(scope: WorkspaceFilesScope): Promise<number> {\n const db = getDbExec();\n const result = await db.execute({\n sql: `SELECT COALESCE(SUM(size_bytes), 0) as total FROM workspace_files WHERE scope = ? AND scope_id = ?`,\n args: [scope.scope, scope.scopeId],\n });\n return Number(result.rows[0]?.[0] ?? 0);\n}\n\nfunction rowToMeta(row: any[]): WorkspaceFileMeta {\n return {\n id: String(row[0]),\n path: String(row[1]),\n contentType: String(row[2] ?? \"text/plain\"),\n sizeBytes: Number(row[3] ?? 0),\n createdAt: String(row[4]),\n updatedAt: String(row[5]),\n };\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `workspace-files` agent tool.
|
|
3
|
+
*
|
|
4
|
+
* A single tool with an `action` discriminator covering write, append, read,
|
|
5
|
+
* list, delete, and grep. Files persist across conversations; the agent uses
|
|
6
|
+
* them to stage large intermediate results (fetched pages, per-item memos)
|
|
7
|
+
* and then read back selectively for synthesis.
|
|
8
|
+
*
|
|
9
|
+
* Scope is automatically resolved from the active request context:
|
|
10
|
+
* - org scope when a request orgId is present (shared across users in the org)
|
|
11
|
+
* - user scope otherwise (personal to the requesting user's email)
|
|
12
|
+
*/
|
|
13
|
+
import type { ActionEntry } from "../agent/production-agent.js";
|
|
14
|
+
export declare function createWorkspaceFilesTool(): Record<string, ActionEntry>;
|
|
15
|
+
//# sourceMappingURL=tool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/workspace-files/tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AA2BhE,wBAAgB,wBAAwB,IAAI,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAiOtE"}
|