dsh-reference-anything 0.2.5 → 0.3.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/NOTICE.md +5 -34
- package/README.md +78 -49
- package/cordis.patch.yml +18 -20
- package/lib/client.js +910 -1439
- package/lib/contract.d.ts +1 -13
- package/lib/contract.js +2 -7
- package/lib/host.d.ts +7 -11
- package/lib/host.js +1 -7
- package/lib/opencli.d.ts +5 -1
- package/lib/opencli.js +26 -43
- package/lib/reference.d.ts +3 -11
- package/lib/reference.js +14 -55
- package/lib/sources/web-chat/index.d.ts +1 -0
- package/lib/sources/web-chat/index.js +5 -0
- package/lib/store/spec.d.ts +9 -0
- package/lib/sync/index.js +4 -4
- package/lib/typert.js +1 -2
- package/lib/web.js +1 -1
- package/lib/wire.d.ts +20 -0
- package/lib/wire.js +13 -7
- package/opencli-plugin/common.js +65 -21
- package/opencli-plugin/opencli-plugin.json +1 -1
- package/opencli-plugin/package.json +1 -1
- package/package.json +17 -12
- package/lib/workspace.d.ts +0 -18
- package/lib/workspace.js +0 -81
package/lib/wire.d.ts
CHANGED
|
@@ -22,36 +22,47 @@ declare const pickerSourceSettingsSchema: z.ZodObject<{
|
|
|
22
22
|
enabled: z.ZodBoolean;
|
|
23
23
|
order: z.ZodNumber;
|
|
24
24
|
limit: z.ZodNumber;
|
|
25
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
25
26
|
}, z.core.$strip>;
|
|
26
27
|
export type PickerSourceSettings = z.infer<typeof pickerSourceSettingsSchema>;
|
|
27
28
|
export declare const pickerSettingsSchema: z.ZodObject<{
|
|
29
|
+
displayMode: z.ZodDefault<z.ZodEnum<{
|
|
30
|
+
collapse: "collapse";
|
|
31
|
+
"native-scroll": "native-scroll";
|
|
32
|
+
}>>;
|
|
28
33
|
commands: z.ZodObject<{
|
|
29
34
|
enabled: z.ZodBoolean;
|
|
30
35
|
order: z.ZodNumber;
|
|
31
36
|
limit: z.ZodNumber;
|
|
37
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
32
38
|
}, z.core.$strip>;
|
|
33
39
|
skills: z.ZodObject<{
|
|
34
40
|
enabled: z.ZodBoolean;
|
|
35
41
|
order: z.ZodNumber;
|
|
36
42
|
limit: z.ZodNumber;
|
|
43
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
37
44
|
}, z.core.$strip>;
|
|
38
45
|
files: z.ZodObject<{
|
|
39
46
|
enabled: z.ZodBoolean;
|
|
40
47
|
order: z.ZodNumber;
|
|
41
48
|
limit: z.ZodNumber;
|
|
49
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
42
50
|
}, z.core.$strip>;
|
|
43
51
|
sessions: z.ZodObject<{
|
|
44
52
|
enabled: z.ZodBoolean;
|
|
45
53
|
order: z.ZodNumber;
|
|
46
54
|
limit: z.ZodNumber;
|
|
55
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
47
56
|
}, z.core.$strip>;
|
|
48
57
|
conversations: z.ZodObject<{
|
|
49
58
|
enabled: z.ZodBoolean;
|
|
50
59
|
order: z.ZodNumber;
|
|
51
60
|
limit: z.ZodNumber;
|
|
61
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
52
62
|
}, z.core.$strip>;
|
|
53
63
|
}, z.core.$strip>;
|
|
54
64
|
export type PickerSettings = z.infer<typeof pickerSettingsSchema>;
|
|
65
|
+
export type PickerDisplayMode = PickerSettings['displayMode'];
|
|
55
66
|
export declare const inputRenderModeSchema: z.ZodEnum<{
|
|
56
67
|
pill: "pill";
|
|
57
68
|
"raw-text": "raw-text";
|
|
@@ -86,30 +97,39 @@ export declare const settingsRecordSchema: z.ZodObject<{
|
|
|
86
97
|
"raw-text": "raw-text";
|
|
87
98
|
}>>;
|
|
88
99
|
picker: z.ZodOptional<z.ZodObject<{
|
|
100
|
+
displayMode: z.ZodDefault<z.ZodEnum<{
|
|
101
|
+
collapse: "collapse";
|
|
102
|
+
"native-scroll": "native-scroll";
|
|
103
|
+
}>>;
|
|
89
104
|
commands: z.ZodObject<{
|
|
90
105
|
enabled: z.ZodBoolean;
|
|
91
106
|
order: z.ZodNumber;
|
|
92
107
|
limit: z.ZodNumber;
|
|
108
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
93
109
|
}, z.core.$strip>;
|
|
94
110
|
skills: z.ZodObject<{
|
|
95
111
|
enabled: z.ZodBoolean;
|
|
96
112
|
order: z.ZodNumber;
|
|
97
113
|
limit: z.ZodNumber;
|
|
114
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
98
115
|
}, z.core.$strip>;
|
|
99
116
|
files: z.ZodObject<{
|
|
100
117
|
enabled: z.ZodBoolean;
|
|
101
118
|
order: z.ZodNumber;
|
|
102
119
|
limit: z.ZodNumber;
|
|
120
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
103
121
|
}, z.core.$strip>;
|
|
104
122
|
sessions: z.ZodObject<{
|
|
105
123
|
enabled: z.ZodBoolean;
|
|
106
124
|
order: z.ZodNumber;
|
|
107
125
|
limit: z.ZodNumber;
|
|
126
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
108
127
|
}, z.core.$strip>;
|
|
109
128
|
conversations: z.ZodObject<{
|
|
110
129
|
enabled: z.ZodBoolean;
|
|
111
130
|
order: z.ZodNumber;
|
|
112
131
|
limit: z.ZodNumber;
|
|
132
|
+
maxCandidates: z.ZodDefault<z.ZodNumber>;
|
|
113
133
|
}, z.core.$strip>;
|
|
114
134
|
}, z.core.$strip>>;
|
|
115
135
|
}, z.core.$strip>;
|
package/lib/wire.js
CHANGED
|
@@ -7,8 +7,10 @@ const pickerSourceSettingsSchema = z.object({
|
|
|
7
7
|
enabled: z.boolean(),
|
|
8
8
|
order: z.number().int().min(0).max(100),
|
|
9
9
|
limit: z.number().int().min(1).max(50),
|
|
10
|
+
maxCandidates: z.number().int().min(1).max(50).default(50),
|
|
10
11
|
});
|
|
11
12
|
export const pickerSettingsSchema = z.object({
|
|
13
|
+
displayMode: z.enum(['collapse', 'native-scroll']).default('collapse'),
|
|
12
14
|
commands: pickerSourceSettingsSchema,
|
|
13
15
|
skills: pickerSourceSettingsSchema,
|
|
14
16
|
files: pickerSourceSettingsSchema,
|
|
@@ -19,18 +21,22 @@ export const inputRenderModeSchema = z.enum(['pill', 'raw-text']);
|
|
|
19
21
|
/** Defaults used before the user saves the General section. */
|
|
20
22
|
export function defaultPickerSettings() {
|
|
21
23
|
return {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
displayMode: 'collapse',
|
|
25
|
+
commands: { enabled: true, order: 0, limit: 6, maxCandidates: 50 },
|
|
26
|
+
skills: { enabled: true, order: 5, limit: 6, maxCandidates: 50 },
|
|
27
|
+
files: { enabled: true, order: 10, limit: 6, maxCandidates: 50 },
|
|
28
|
+
sessions: { enabled: true, order: 20, limit: 6, maxCandidates: 50 },
|
|
29
|
+
conversations: { enabled: true, order: 30, limit: 6, maxCandidates: 50 },
|
|
27
30
|
};
|
|
28
31
|
}
|
|
29
32
|
/** Picker settings have a fixed schema, so explicit comparison is cheap and stable. */
|
|
30
33
|
export function samePickerSettings(left, right) {
|
|
31
|
-
|
|
34
|
+
if (left.displayMode !== right.displayMode)
|
|
35
|
+
return false;
|
|
36
|
+
return pickerSourceSchema.options.every(key => left[key].enabled === right[key].enabled &&
|
|
32
37
|
left[key].order === right[key].order &&
|
|
33
|
-
left[key].limit === right[key].limit
|
|
38
|
+
left[key].limit === right[key].limit &&
|
|
39
|
+
left[key].maxCandidates === right[key].maxCandidates);
|
|
34
40
|
}
|
|
35
41
|
export const settingsRecordSchema = z.object({
|
|
36
42
|
opencliPath: z.string(),
|
package/opencli-plugin/common.js
CHANGED
|
@@ -49,10 +49,54 @@ function assertResult(result, domain, operation) {
|
|
|
49
49
|
return result.rows
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
const PROVIDER_READY_TIMEOUT_MS = 1200
|
|
53
|
+
const PROVIDER_READY_INITIAL_DELAY_MS = 50
|
|
54
|
+
const PROVIDER_READY_MAX_DELAY_MS = 400
|
|
55
|
+
|
|
56
|
+
function terminalProviderResult(result) {
|
|
57
|
+
return result?.ok === true || result?.code === 'AUTH' || result?.code === 'RATE_LIMIT'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function terminalIdentityResult(result) {
|
|
61
|
+
return result?.code === 'AUTH' || result?.code === 'RATE_LIMIT'
|
|
62
|
+
|| (result?.ok === true && typeof result.identity === 'string' && result.identity.length > 0)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Run the provider API as the readiness probe. The happy path performs one
|
|
67
|
+
* evaluate immediately after navigation; transient bootstrap failures retry
|
|
68
|
+
* with bounded exponential backoff instead of waiting for the whole SPA DOM
|
|
69
|
+
* to become quiet first.
|
|
70
|
+
*/
|
|
71
|
+
export async function evaluateWhenProviderReady(page, script, args, operation, options = {}) {
|
|
72
|
+
const timeoutMs = options.timeoutMs ?? PROVIDER_READY_TIMEOUT_MS
|
|
73
|
+
const maxDelayMs = options.maxDelayMs ?? PROVIDER_READY_MAX_DELAY_MS
|
|
74
|
+
const accepts = options.accepts ?? terminalProviderResult
|
|
75
|
+
const deadline = Date.now() + timeoutMs
|
|
76
|
+
let delayMs = options.initialDelayMs ?? PROVIDER_READY_INITIAL_DELAY_MS
|
|
77
|
+
let lastResult
|
|
78
|
+
let lastError
|
|
79
|
+
for (;;) {
|
|
80
|
+
try {
|
|
81
|
+
lastResult = await evaluate(page, script, args, operation)
|
|
82
|
+
lastError = undefined
|
|
83
|
+
if (accepts(lastResult)) return lastResult
|
|
84
|
+
} catch (error) {
|
|
85
|
+
lastError = error
|
|
86
|
+
}
|
|
87
|
+
const remaining = deadline - Date.now()
|
|
88
|
+
if (remaining <= 0) {
|
|
89
|
+
if (lastError) throw lastError
|
|
90
|
+
return lastResult
|
|
91
|
+
}
|
|
92
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(delayMs, remaining)))
|
|
93
|
+
delayMs = Math.min(Math.max(delayMs * 2, 1), maxDelayMs)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
52
97
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* Browser Bridge can keep and reuse the background container window.
|
|
98
|
+
* Each command owns an ephemeral Browser Bridge tab. Close that tab after the
|
|
99
|
+
* command finishes while preserving the provider result or failure.
|
|
56
100
|
*/
|
|
57
101
|
async function inTemporaryTab(page, task) {
|
|
58
102
|
try {
|
|
@@ -80,10 +124,12 @@ export function registerProvider(config) {
|
|
|
80
124
|
...browserSession,
|
|
81
125
|
navigateBefore: false,
|
|
82
126
|
args: [],
|
|
83
|
-
columns: ['identity'],
|
|
127
|
+
columns: ['identity'],
|
|
84
128
|
func: async (page) => inTemporaryTab(page, async () => {
|
|
85
|
-
await page.goto(config.home, {
|
|
86
|
-
const result = await
|
|
129
|
+
await page.goto(config.home, { waitUntil: 'none' })
|
|
130
|
+
const result = await evaluateWhenProviderReady(
|
|
131
|
+
page, config.whoamiScript, {}, `${config.site} whoami`, { accepts: terminalIdentityResult },
|
|
132
|
+
)
|
|
87
133
|
if (result?.code === 'AUTH') throw new AuthRequiredError(config.domain)
|
|
88
134
|
if (result?.ok !== true || typeof result.identity !== 'string' || !result.identity) {
|
|
89
135
|
throw new CommandExecutionError(`${config.site} whoami: ${result?.message || 'stable account identity unavailable'}`)
|
|
@@ -110,8 +156,10 @@ export function registerProvider(config) {
|
|
|
110
156
|
func: async (page, kwargs) => inTemporaryTab(page, async () => {
|
|
111
157
|
const since = String(kwargs.since || '').trim()
|
|
112
158
|
if (since && Number.isNaN(Date.parse(since))) throw new ArgumentError('since must be an ISO 8601 instant')
|
|
113
|
-
await page.goto(config.home, {
|
|
114
|
-
const identity = await
|
|
159
|
+
await page.goto(config.home, { waitUntil: 'none' })
|
|
160
|
+
const identity = await evaluateWhenProviderReady(
|
|
161
|
+
page, config.whoamiScript, {}, `${config.site} sync-index identity`, { accepts: terminalIdentityResult },
|
|
162
|
+
)
|
|
115
163
|
if (identity?.code === 'AUTH') throw new AuthRequiredError(config.domain)
|
|
116
164
|
if (identity?.ok !== true || typeof identity.identity !== 'string' || !identity.identity) {
|
|
117
165
|
throw new CommandExecutionError(`${config.site} sync-index: ${identity?.message || 'stable account identity unavailable'}`)
|
|
@@ -147,12 +195,12 @@ export function registerProvider(config) {
|
|
|
147
195
|
columns: HISTORY_COLUMNS,
|
|
148
196
|
func: async (page, kwargs) => inTemporaryTab(page, async () => {
|
|
149
197
|
const since = String(kwargs.since || '').trim()
|
|
150
|
-
if (since && Number.isNaN(Date.parse(since))) throw new ArgumentError('since must be an ISO 8601 instant')
|
|
151
|
-
await page.goto(config.home, {
|
|
152
|
-
const rows = assertResult(
|
|
153
|
-
await
|
|
154
|
-
config.domain,
|
|
155
|
-
`${config.site} history-all`,
|
|
198
|
+
if (since && Number.isNaN(Date.parse(since))) throw new ArgumentError('since must be an ISO 8601 instant')
|
|
199
|
+
await page.goto(config.home, { waitUntil: 'none' })
|
|
200
|
+
const rows = assertResult(
|
|
201
|
+
await evaluateWhenProviderReady(page, config.historyScript, { since }, `${config.site} history-all`),
|
|
202
|
+
config.domain,
|
|
203
|
+
`${config.site} history-all`,
|
|
156
204
|
)
|
|
157
205
|
return rows
|
|
158
206
|
}),
|
|
@@ -247,16 +295,12 @@ export function registerProvider(config) {
|
|
|
247
295
|
localPath: output,
|
|
248
296
|
}]
|
|
249
297
|
}),
|
|
250
|
-
})
|
|
298
|
+
})
|
|
251
299
|
}
|
|
252
300
|
|
|
253
|
-
/**
|
|
254
|
-
* Keep one stable, background Browser Bridge session per provider site.
|
|
255
|
-
* Persistence suppresses OpenCLI's `close-window` action; inTemporaryTab closes
|
|
256
|
-
* only the finished command's tab so the reusable container window stays alive.
|
|
257
|
-
*/
|
|
301
|
+
/** Browser Bridge lifecycle shared by every web-chat command. */
|
|
258
302
|
export const SYNC_BROWSER_SESSION = Object.freeze({
|
|
259
|
-
siteSession: '
|
|
303
|
+
siteSession: 'ephemeral',
|
|
260
304
|
defaultWindowMode: 'background',
|
|
261
305
|
})
|
|
262
306
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-reference-anything",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Reference locally mirrored ChatGPT, Claude, Gemini, DeepSeek, Grok, and Kimi conversations from DeepSeek Harness",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -85,30 +85,34 @@
|
|
|
85
85
|
"peerDependencies": {
|
|
86
86
|
"@deepseek-ai/cordis": ">=4.0.1",
|
|
87
87
|
"@deepseek-ai/dsh-agent": ">=0.1.0-rc.6",
|
|
88
|
-
"@deepseek-ai/dsh-api-remotes": ">=0.0
|
|
88
|
+
"@deepseek-ai/dsh-api-remotes": ">=0.1.0-rc.8",
|
|
89
89
|
"@deepseek-ai/dsh-attachment": ">=0.1.0-rc.6",
|
|
90
|
-
"@deepseek-ai/dsh-client-connection": ">=0.0
|
|
91
|
-
"@deepseek-ai/dsh-client-locale": ">=0.0
|
|
92
|
-
"@deepseek-ai/dsh-client-runtime": ">=0.0
|
|
93
|
-
"@deepseek-ai/dsh-client-ui-conversation": ">=0.0
|
|
94
|
-
"@deepseek-ai/dsh-client-ui-input-trigger": ">=0.0
|
|
95
|
-
"@deepseek-ai/dsh-client-ui-settings": ">=0.0
|
|
96
|
-
"@deepseek-ai/dsh-client-ui-slots": ">=0.0
|
|
90
|
+
"@deepseek-ai/dsh-client-connection": ">=0.1.0-rc.8",
|
|
91
|
+
"@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.8",
|
|
92
|
+
"@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.8",
|
|
93
|
+
"@deepseek-ai/dsh-client-ui-conversation": ">=0.1.0-rc.8",
|
|
94
|
+
"@deepseek-ai/dsh-client-ui-input-trigger": ">=0.1.0-rc.8",
|
|
95
|
+
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.0-rc.8",
|
|
96
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.0-rc.8",
|
|
97
|
+
"@deepseek-ai/dsh-file-reference": ">=0.1.0-rc.8",
|
|
97
98
|
"@deepseek-ai/dsh-llm": ">=0.1.0-rc.6",
|
|
98
99
|
"@deepseek-ai/dsh-output-retention": ">=0.1.0-rc.6",
|
|
99
100
|
"@deepseek-ai/dsh-session": ">=0.1.0-rc.6",
|
|
100
101
|
"@deepseek-ai/dsh-session-reference": ">=0.1.0-rc.6",
|
|
101
|
-
"@deepseek-ai/dsh-settings": ">=0.0
|
|
102
|
-
"@deepseek-ai/dsh-storage-domain": ">=0.0
|
|
102
|
+
"@deepseek-ai/dsh-settings": ">=0.1.0-rc.8",
|
|
103
|
+
"@deepseek-ai/dsh-storage-domain": ">=0.1.0-rc.8",
|
|
103
104
|
"@deepseek-ai/dsh-tools": ">=0.1.0-rc.6",
|
|
104
105
|
"@deepseek-ai/dsh-typert-protocol": ">=0.1.0-rc.6",
|
|
105
|
-
"@deepseek-ai/dsh-typert-registry": ">=0.0
|
|
106
|
+
"@deepseek-ai/dsh-typert-registry": ">=0.1.0-rc.8",
|
|
106
107
|
"react": "^18.2.0"
|
|
107
108
|
},
|
|
108
109
|
"peerDependenciesMeta": {
|
|
109
110
|
"@deepseek-ai/dsh-session-reference": {
|
|
110
111
|
"optional": true
|
|
111
112
|
},
|
|
113
|
+
"@deepseek-ai/dsh-file-reference": {
|
|
114
|
+
"optional": true
|
|
115
|
+
},
|
|
112
116
|
"@deepseek-ai/dsh-client-runtime": {
|
|
113
117
|
"optional": true
|
|
114
118
|
},
|
|
@@ -157,6 +161,7 @@
|
|
|
157
161
|
"@deepseek-ai/dsh-client-ui-input-trigger": "link:../deepseek-harness/packages/client/ui-input-trigger",
|
|
158
162
|
"@deepseek-ai/dsh-client-ui-settings": "link:../deepseek-harness/packages/client/ui-settings",
|
|
159
163
|
"@deepseek-ai/dsh-client-ui-slots": "link:../deepseek-harness/packages/client/ui-slots",
|
|
164
|
+
"@deepseek-ai/dsh-file-reference": "link:../deepseek-harness/packages/context/file-reference",
|
|
160
165
|
"@deepseek-ai/dsh-llm": "link:../deepseek-harness/packages/llm/llm",
|
|
161
166
|
"@deepseek-ai/dsh-output-retention": "link:../deepseek-harness/packages/util/output-retention",
|
|
162
167
|
"@deepseek-ai/dsh-session": "link:../deepseek-harness/packages/core/session",
|
package/lib/workspace.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
2
|
-
export declare const WORKSPACE_REFERENCE_SCHEME = "dsh-file:";
|
|
3
|
-
export interface WorkspaceEntry {
|
|
4
|
-
path: string;
|
|
5
|
-
kind: 'file' | 'directory';
|
|
6
|
-
}
|
|
7
|
-
export interface WorkspaceReference {
|
|
8
|
-
path: string;
|
|
9
|
-
label: string;
|
|
10
|
-
}
|
|
11
|
-
export declare function indexWorkspace(agent: Agent, signal: AbortSignal, maxFiles?: number): Promise<WorkspaceEntry[]>;
|
|
12
|
-
export declare function encodeWorkspaceReferenceUri(path: string): string;
|
|
13
|
-
export declare function formatWorkspaceMention(reference: WorkspaceReference): string;
|
|
14
|
-
export declare function parseWorkspaceReferenceText(text: string): {
|
|
15
|
-
text: string;
|
|
16
|
-
references: WorkspaceReference[];
|
|
17
|
-
};
|
|
18
|
-
export declare function renderWorkspaceReferences(agent: Agent, references: readonly WorkspaceReference[]): Promise<string>;
|
package/lib/workspace.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { lstat, readdir } from 'node:fs/promises';
|
|
2
|
-
import { basename, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
|
-
export const WORKSPACE_REFERENCE_SCHEME = 'dsh-file:';
|
|
4
|
-
const IGNORED = new Set(['.git', '.hg', '.svn', 'node_modules', 'dist', 'build', 'coverage', '.next', '.cache', '.idea', '.vscode', '__pycache__']);
|
|
5
|
-
const IGNORED_FILES = new Set(['desktop.ini', 'Thumbs.db', '.DS_Store']);
|
|
6
|
-
export async function indexWorkspace(agent, signal, maxFiles = 5000) {
|
|
7
|
-
const root = agent.session.header.cwd;
|
|
8
|
-
if (!root || !isAbsolute(root))
|
|
9
|
-
return [];
|
|
10
|
-
const output = [];
|
|
11
|
-
const queue = ['.'];
|
|
12
|
-
while (queue.length > 0 && output.length < maxFiles) {
|
|
13
|
-
signal.throwIfAborted();
|
|
14
|
-
const current = queue.shift();
|
|
15
|
-
let entries;
|
|
16
|
-
try {
|
|
17
|
-
entries = await readdir(resolve(root, current), { withFileTypes: true });
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
23
|
-
for (const entry of entries) {
|
|
24
|
-
if (output.length >= maxFiles)
|
|
25
|
-
break;
|
|
26
|
-
if (entry.isSymbolicLink() || IGNORED_FILES.has(entry.name))
|
|
27
|
-
continue;
|
|
28
|
-
const path = current === '.' ? entry.name : `${current}/${entry.name}`;
|
|
29
|
-
if (entry.isDirectory()) {
|
|
30
|
-
if (IGNORED.has(entry.name))
|
|
31
|
-
continue;
|
|
32
|
-
output.push({ path, kind: 'directory' });
|
|
33
|
-
queue.push(path);
|
|
34
|
-
}
|
|
35
|
-
else if (entry.isFile())
|
|
36
|
-
output.push({ path, kind: 'file' });
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return output;
|
|
40
|
-
}
|
|
41
|
-
export function encodeWorkspaceReferenceUri(path) {
|
|
42
|
-
return `${WORKSPACE_REFERENCE_SCHEME}${Buffer.from(JSON.stringify(path), 'utf8').toString('base64url')}`;
|
|
43
|
-
}
|
|
44
|
-
export function formatWorkspaceMention(reference) {
|
|
45
|
-
return `@[${reference.label.replace(/[\\\]]/gu, value => `\\${value}`)}](${encodeWorkspaceReferenceUri(reference.path)})`;
|
|
46
|
-
}
|
|
47
|
-
export function parseWorkspaceReferenceText(text) {
|
|
48
|
-
const references = [];
|
|
49
|
-
const pattern = /@\[((?:\\.|[^\\\]])*)\]\((dsh-file:[^\s)]*)\)|(dsh-file:[A-Za-z0-9_-]+)/gu;
|
|
50
|
-
const rendered = text.replace(pattern, (_match, rawLabel, markdownUri, bareUri) => {
|
|
51
|
-
const uri = markdownUri ?? bareUri;
|
|
52
|
-
const payload = uri.slice(WORKSPACE_REFERENCE_SCHEME.length);
|
|
53
|
-
if (!/^[A-Za-z0-9_-]+$/u.test(payload))
|
|
54
|
-
throw new Error(`invalid workspace reference URI ${JSON.stringify(uri)}`);
|
|
55
|
-
const path = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
56
|
-
if (typeof path !== 'string' || path.length === 0 || encodeWorkspaceReferenceUri(path) !== uri)
|
|
57
|
-
throw new Error(`invalid workspace reference URI ${JSON.stringify(uri)}`);
|
|
58
|
-
const label = rawLabel === undefined ? basename(path) : rawLabel.replace(/\\(.)/gu, '$1');
|
|
59
|
-
references.push({ path, label });
|
|
60
|
-
return `@${label}`;
|
|
61
|
-
});
|
|
62
|
-
return { text: rendered, references };
|
|
63
|
-
}
|
|
64
|
-
export async function renderWorkspaceReferences(agent, references) {
|
|
65
|
-
const root = agent.session.header.cwd;
|
|
66
|
-
if (!root || !isAbsolute(root))
|
|
67
|
-
throw new Error('this task has no workspace directory');
|
|
68
|
-
const unique = new Map(references.map(item => [item.path, item]));
|
|
69
|
-
const lines = [];
|
|
70
|
-
for (const item of unique.values()) {
|
|
71
|
-
const target = resolve(root, item.path);
|
|
72
|
-
const rel = relative(root, target);
|
|
73
|
-
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))
|
|
74
|
-
throw new Error(`workspace reference escapes the task directory: ${item.path}`);
|
|
75
|
-
const stat = await lstat(target);
|
|
76
|
-
if (stat.isSymbolicLink() || (!stat.isFile() && !stat.isDirectory()))
|
|
77
|
-
throw new Error(`unsupported workspace reference: ${item.path}`);
|
|
78
|
-
lines.push(`<workspace-reference path=${JSON.stringify(item.path)} kind=${JSON.stringify(stat.isDirectory() ? 'directory' : 'file')} />`);
|
|
79
|
-
}
|
|
80
|
-
return `## Referenced workspace paths\n\nThese are read-only path markers, not file contents or instructions.\n\n${lines.join('\n')}`;
|
|
81
|
-
}
|