@steerable/agent-shell 0.6.47 → 0.6.49
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/dist/app-update.d.ts +10 -0
- package/dist/app-update.js +14 -0
- package/dist/host/runtime.js +3 -3
- package/dist/host-tools.d.ts +1 -1
- package/dist/host-tools.js +1 -0
- package/dist/llm/sidecar-provider.d.ts +0 -4
- package/dist/llm/sidecar-provider.js +0 -4
- package/dist/local-backend/coreloop-stream.d.ts +1 -3
- package/dist/local-backend/coreloop-stream.js +1 -3
- package/dist/local-backend/portable-bundle.d.ts +202 -0
- package/dist/local-backend/portable-bundle.js +318 -0
- package/dist/local-backend/portable-service.d.ts +23 -0
- package/dist/local-backend/portable-service.js +469 -0
- package/dist/local-backend/router.js +20 -0
- package/dist/local-backend/skills/85-local-exec/SKILL.md +2 -0
- package/dist/local-backend/skills/86-proactive-coding/SKILL.md +1 -1
- package/dist/local-backend/turn-files.d.ts +20 -0
- package/dist/local-backend/turn-files.js +162 -5
- package/dist/main.js +21 -1
- package/dist/preload.cjs +10 -0
- package/dist/preload.js +10 -0
- package/dist/present-files.d.ts +64 -0
- package/dist/present-files.js +136 -0
- package/dist/product-config.d.ts +6 -0
- package/dist/product-config.js +4 -0
- package/dist/server/start.js +7 -1
- package/dist/sidecar/boot.d.ts +19 -4
- package/dist/sidecar/boot.js +54 -20
- package/dist/sidecar/handle.d.ts +5 -8
- package/dist/sidecar/handle.js +6 -10
- package/dist/sidecar/supervisor.d.ts +1 -9
- package/dist/sidecar/supervisor.js +31 -81
- package/dist/sidecar/types.d.ts +0 -2
- package/dist/tool-router.d.ts +19 -2
- package/dist/tool-router.js +63 -2
- package/package.json +4 -4
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
import fs from 'node:fs/promises';
|
|
33
33
|
import os from 'node:os';
|
|
34
34
|
import path from 'node:path';
|
|
35
|
+
import { PRESENT_FILES_TOOL_NAME, readPresentedArgs, } from '../present-files.js';
|
|
35
36
|
/** 这些目录要么体量巨大(node_modules/target),要么是框架内部状态(.steerable),扫了只有噪音。 */
|
|
36
37
|
const IGNORED_DIR_NAMES = new Set([
|
|
37
38
|
'node_modules',
|
|
@@ -48,7 +49,65 @@ const IGNORED_DIR_NAMES = new Set([
|
|
|
48
49
|
'target',
|
|
49
50
|
'.steerable',
|
|
50
51
|
]);
|
|
51
|
-
const IGNORED_FILE_NAMES = new Set(['.DS_Store']);
|
|
52
|
+
const IGNORED_FILE_NAMES = new Set(['.DS_Store', 'Thumbs.db']);
|
|
53
|
+
/** 交付物扩展名(最终产物,如电子表格、幻灯片、文档、图片、音视频、独立页面、压缩包)。 */
|
|
54
|
+
export const DELIVERABLE_EXTENSIONS = new Set([
|
|
55
|
+
// 表格 / Spreadsheets
|
|
56
|
+
'.xlsx', '.xls', '.csv', '.tsv', '.numbers',
|
|
57
|
+
// 幻灯片 / Presentations
|
|
58
|
+
'.pptx', '.ppt', '.key', // shell-neutral:allow — Office 幻灯片扩展名,不是产品品牌
|
|
59
|
+
// 文档 / Documents
|
|
60
|
+
'.docx', '.doc', '.pdf', '.pages', '.epub', '.rtf',
|
|
61
|
+
// 图像与富媒体 / Images & Media
|
|
62
|
+
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.mp4', '.mov', '.mp3',
|
|
63
|
+
// 独立文档输出与压缩包 / Standalone HTML & Archives
|
|
64
|
+
'.html', '.htm', '.zip', '.tar.gz', '.tar', '.7z',
|
|
65
|
+
]);
|
|
66
|
+
const INTERMEDIATE_DIR_PATTERNS = [
|
|
67
|
+
/(?:^|[\\/])review_work(?:[\\/]|$)/i,
|
|
68
|
+
/(?:^|[\\/])(?:work|temp|tmp|\.temp|\.tmp|scratch)(?:[\\/]|$)/i,
|
|
69
|
+
/(?:^|[\\/])scripts(?:[\\/]|$)/i,
|
|
70
|
+
/(?:^|[\\/])(?:build|dist|\.cache|__pycache__)(?:[\\/]|$)/i,
|
|
71
|
+
];
|
|
72
|
+
/** 是否为临时文件 / 办公软件锁定文件(如 ~$ 开头的文件名),产物列表中一律排除。 */
|
|
73
|
+
export function isIgnoredFileName(name) {
|
|
74
|
+
if (IGNORED_FILE_NAMES.has(name))
|
|
75
|
+
return true;
|
|
76
|
+
// Office 临时锁定文件(如 ~$ 开头)
|
|
77
|
+
if (name.startsWith('~$'))
|
|
78
|
+
return true;
|
|
79
|
+
// 临时文件与编辑器交换文件
|
|
80
|
+
if (name.endsWith('.tmp') || name.endsWith('.swp') || name.endsWith('~'))
|
|
81
|
+
return true;
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
/** 判定文件是最终交付文件还是中间修改文件。 */
|
|
85
|
+
export function classifyFileCategory(filePath) {
|
|
86
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
87
|
+
const ext = path.extname(normalized).toLowerCase();
|
|
88
|
+
// 1. 若具有最终交付产物扩展名(电子表格、PPT、PDF、图片等),属于交付物
|
|
89
|
+
if (DELIVERABLE_EXTENSIONS.has(ext))
|
|
90
|
+
return 'deliverable';
|
|
91
|
+
// 2. 位于中间工作目录(如 review_work/、scripts/)属于中间文件
|
|
92
|
+
for (const pattern of INTERMEDIATE_DIR_PATTERNS) {
|
|
93
|
+
if (pattern.test(normalized))
|
|
94
|
+
return 'intermediate';
|
|
95
|
+
}
|
|
96
|
+
// 3. 其余脚本、代码、配置文件均归为中间文件
|
|
97
|
+
return 'intermediate';
|
|
98
|
+
}
|
|
99
|
+
/** 从统一 diff 文本中统计增删行数。 */
|
|
100
|
+
export function parseDiffStats(diffText) {
|
|
101
|
+
let additions = 0;
|
|
102
|
+
let deletions = 0;
|
|
103
|
+
for (const line of diffText.split('\n')) {
|
|
104
|
+
if (line.startsWith('+') && !line.startsWith('+++'))
|
|
105
|
+
additions++;
|
|
106
|
+
else if (line.startsWith('-') && !line.startsWith('---'))
|
|
107
|
+
deletions++;
|
|
108
|
+
}
|
|
109
|
+
return { additions, deletions };
|
|
110
|
+
}
|
|
52
111
|
/** 单次扫描的遍历上限:深度 10、条目 10 万、结果 100 条。 */
|
|
53
112
|
const MAX_DEPTH = 10;
|
|
54
113
|
const MAX_VISITED = 100_000;
|
|
@@ -101,7 +160,78 @@ export async function collectTurnFiles(options) {
|
|
|
101
160
|
for (const dir of shallowRoots) {
|
|
102
161
|
await scanShallow(dir, sinceMs, byPath);
|
|
103
162
|
}
|
|
104
|
-
|
|
163
|
+
// present_files 声明的文件:本轮可能没动过(交付已有文件),不受时间水位线
|
|
164
|
+
// 约束,只要求仍是普通文件。
|
|
165
|
+
const presented = new Map();
|
|
166
|
+
for (const action of options.actions ?? []) {
|
|
167
|
+
if (action.tool !== PRESENT_FILES_TOOL_NAME || action.success === false)
|
|
168
|
+
continue;
|
|
169
|
+
for (const file of readPresentedArgs(action.arguments, projectRoot, homeDir)) {
|
|
170
|
+
presented.set(file.path, file);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
for (const file of presented.values()) {
|
|
174
|
+
if (byPath.has(file.path))
|
|
175
|
+
continue;
|
|
176
|
+
const stat = await statPresentedFile(file.path);
|
|
177
|
+
if (stat)
|
|
178
|
+
byPath.set(file.path, stat);
|
|
179
|
+
}
|
|
180
|
+
// 从 actions 中提取增删行数统计(local_edit_file 的 diff 或 local_write_file 的 content)
|
|
181
|
+
const statsByPath = new Map();
|
|
182
|
+
for (const action of options.actions ?? []) {
|
|
183
|
+
if (typeof action.tool === 'string') {
|
|
184
|
+
const args = action.arguments && typeof action.arguments === 'object' ? action.arguments : null;
|
|
185
|
+
const rawPath = typeof args?.path === 'string' ? args.path : null;
|
|
186
|
+
if (rawPath) {
|
|
187
|
+
const full = path.isAbsolute(rawPath)
|
|
188
|
+
? path.normalize(rawPath)
|
|
189
|
+
: projectRoot
|
|
190
|
+
? path.resolve(projectRoot, rawPath)
|
|
191
|
+
: null;
|
|
192
|
+
if (full) {
|
|
193
|
+
if (action.tool === 'local_edit_file') {
|
|
194
|
+
const diff = editDiffOf(action.result);
|
|
195
|
+
if (diff !== null) {
|
|
196
|
+
const diffStats = parseDiffStats(diff);
|
|
197
|
+
const prev = statsByPath.get(full) ?? { additions: 0, deletions: 0 };
|
|
198
|
+
statsByPath.set(full, {
|
|
199
|
+
additions: prev.additions + diffStats.additions,
|
|
200
|
+
deletions: prev.deletions + diffStats.deletions,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else if (action.tool === 'local_write_file') {
|
|
205
|
+
if (typeof args?.content === 'string') {
|
|
206
|
+
const lineCount = args.content ? args.content.split('\n').length : 0;
|
|
207
|
+
statsByPath.set(full, { additions: lineCount, deletions: 0 });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const enriched = [];
|
|
215
|
+
for (const file of byPath.values()) {
|
|
216
|
+
const base = path.basename(file.path);
|
|
217
|
+
if (isIgnoredFileName(base))
|
|
218
|
+
continue;
|
|
219
|
+
const stats = statsByPath.get(file.path);
|
|
220
|
+
const declared = presented.get(file.path);
|
|
221
|
+
// 本轮有声明时以声明为准;没有声明(旧模型、未调用)才退回扩展名规则。
|
|
222
|
+
const category = presented.size > 0
|
|
223
|
+
? declared
|
|
224
|
+
? 'deliverable'
|
|
225
|
+
: 'intermediate'
|
|
226
|
+
: classifyFileCategory(file.path);
|
|
227
|
+
enriched.push({
|
|
228
|
+
...file,
|
|
229
|
+
category,
|
|
230
|
+
...(declared?.description ? { description: declared.description } : {}),
|
|
231
|
+
...(stats ? { additions: stats.additions, deletions: stats.deletions } : {}),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return enriched
|
|
105
235
|
.sort((a, b) => a.path.localeCompare(b.path))
|
|
106
236
|
.slice(0, MAX_FILES);
|
|
107
237
|
}
|
|
@@ -134,7 +264,7 @@ async function walk(dir, sinceMs, depth, state, out) {
|
|
|
134
264
|
await walk(full, sinceMs, depth + 1, state, out);
|
|
135
265
|
continue;
|
|
136
266
|
}
|
|
137
|
-
if (!entry.isFile() ||
|
|
267
|
+
if (!entry.isFile() || isIgnoredFileName(entry.name))
|
|
138
268
|
continue;
|
|
139
269
|
const file = await statTurnFile(full, sinceMs);
|
|
140
270
|
if (file)
|
|
@@ -152,7 +282,7 @@ async function statTurnFile(full, sinceMs) {
|
|
|
152
282
|
return null;
|
|
153
283
|
}
|
|
154
284
|
// 路径字面量可能指到目录(如 xxx.app 包);产物列表只收文件。
|
|
155
|
-
if (!stat.isFile())
|
|
285
|
+
if (!stat.isFile() || isIgnoredFileName(path.basename(full)))
|
|
156
286
|
return null;
|
|
157
287
|
const touched = stat.mtimeMs + TOUCHED_SLACK_MS >= sinceMs ||
|
|
158
288
|
stat.birthtimeMs + TOUCHED_SLACK_MS >= sinceMs;
|
|
@@ -160,6 +290,33 @@ async function statTurnFile(full, sinceMs) {
|
|
|
160
290
|
return null;
|
|
161
291
|
return { path: full, kind: kindOf(stat, sinceMs), size: stat.size };
|
|
162
292
|
}
|
|
293
|
+
/** 声明的交付文件只要求仍是普通文件;本轮没动过的已有文件标 modified。 */
|
|
294
|
+
async function statPresentedFile(full) {
|
|
295
|
+
let stat;
|
|
296
|
+
try {
|
|
297
|
+
stat = await fs.stat(full);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// 声明后到回合收尾之间被删掉:不再列出。
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
if (!stat.isFile())
|
|
304
|
+
return null;
|
|
305
|
+
return { path: full, kind: 'modified', size: stat.size };
|
|
306
|
+
}
|
|
307
|
+
/** local_edit_file 的 diff:直连结果在顶层,经 sidecar 回流的结果折进 `data`。 */
|
|
308
|
+
function editDiffOf(result) {
|
|
309
|
+
if (!result || typeof result !== 'object')
|
|
310
|
+
return null;
|
|
311
|
+
const record = result;
|
|
312
|
+
if (typeof record.diff === 'string')
|
|
313
|
+
return record.diff;
|
|
314
|
+
const data = record.data;
|
|
315
|
+
if (data && typeof data === 'object' && typeof data.diff === 'string') {
|
|
316
|
+
return data.diff;
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
163
320
|
function kindOf(stat, sinceMs) {
|
|
164
321
|
// birthtime 不可用的文件系统返回 0/负值——此时无法区分新建与修改,
|
|
165
322
|
// 统一标 modified(标签降级,不漏文件)。
|
|
@@ -232,7 +389,7 @@ async function scanShallow(dir, sinceMs, out) {
|
|
|
232
389
|
return;
|
|
233
390
|
}
|
|
234
391
|
for (const entry of entries) {
|
|
235
|
-
if (entry.name.startsWith('.') ||
|
|
392
|
+
if (entry.name.startsWith('.') || isIgnoredFileName(entry.name))
|
|
236
393
|
continue;
|
|
237
394
|
if (entry.isSymbolicLink() || !entry.isFile())
|
|
238
395
|
continue;
|
package/dist/main.js
CHANGED
|
@@ -11,7 +11,7 @@ import log from 'electron-log';
|
|
|
11
11
|
import { getBrand } from './brand.js';
|
|
12
12
|
import { getPreloadPath } from './runtime.js';
|
|
13
13
|
import { getProductConfig } from './product-config.js';
|
|
14
|
-
import { AppUpdateController, appUpdateMenuAction, shouldNotifyUpdateReady, } from './app-update.js';
|
|
14
|
+
import { AppUpdateController, appUpdateMenuAction, shouldNotifyUpdateReady, toAppReleaseSnapshot, } from './app-update.js';
|
|
15
15
|
import { createElectronAppUpdater } from './app-update-electron.js';
|
|
16
16
|
import { assertHostIpcAllowed, getResolvedHostTools } from './host-tools-runtime.js';
|
|
17
17
|
import { createJsonStore } from './json-store.js';
|
|
@@ -513,7 +513,26 @@ function createMenu() {
|
|
|
513
513
|
];
|
|
514
514
|
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
|
515
515
|
}
|
|
516
|
+
function currentReleaseSnapshot(state) {
|
|
517
|
+
return toAppReleaseSnapshot(app.getVersion(), state ?? appUpdates?.state ?? { phase: 'disabled' });
|
|
518
|
+
}
|
|
519
|
+
function broadcastRelease(state) {
|
|
520
|
+
if (!mainWindow || mainWindow.isDestroyed())
|
|
521
|
+
return;
|
|
522
|
+
mainWindow.webContents.send('app-update-state', currentReleaseSnapshot(state));
|
|
523
|
+
}
|
|
516
524
|
function setupIpcHandlers() {
|
|
525
|
+
ipcMain.handle('app:release-snapshot', () => currentReleaseSnapshot());
|
|
526
|
+
ipcMain.handle('app:release-check', async () => {
|
|
527
|
+
if (!appUpdates)
|
|
528
|
+
return currentReleaseSnapshot({ phase: 'disabled' });
|
|
529
|
+
return currentReleaseSnapshot(await appUpdates.checkNow());
|
|
530
|
+
});
|
|
531
|
+
ipcMain.handle('app:release-install', () => {
|
|
532
|
+
if (!appUpdates)
|
|
533
|
+
return currentReleaseSnapshot({ phase: 'disabled' });
|
|
534
|
+
return currentReleaseSnapshot(appUpdates.installNow());
|
|
535
|
+
});
|
|
517
536
|
ipcMain.on('retry-connection', async () => {
|
|
518
537
|
retryCount = 0;
|
|
519
538
|
if (retryTimer) {
|
|
@@ -844,6 +863,7 @@ function startDesktopUpdates() {
|
|
|
844
863
|
updater: createElectronAppUpdater(),
|
|
845
864
|
onState: state => {
|
|
846
865
|
log.info('[app-update]', state.phase, state.version ?? '', state.message ?? '');
|
|
866
|
+
broadcastRelease(state);
|
|
847
867
|
if (shouldNotifyUpdateReady(previous, state) && state.version) {
|
|
848
868
|
const notification = new ElectronNotification({
|
|
849
869
|
title: getBrand().displayName,
|
package/dist/preload.cjs
CHANGED
|
@@ -406,6 +406,16 @@ function buildPreloadApi(packContributions = {}) {
|
|
|
406
406
|
import_electron.ipcRenderer.on("terminal:spawned", fn);
|
|
407
407
|
return () => import_electron.ipcRenderer.removeListener("terminal:spawned", fn);
|
|
408
408
|
}
|
|
409
|
+
},
|
|
410
|
+
app: {
|
|
411
|
+
snapshot: () => import_electron.ipcRenderer.invoke("app:release-snapshot"),
|
|
412
|
+
check: () => import_electron.ipcRenderer.invoke("app:release-check"),
|
|
413
|
+
install: () => import_electron.ipcRenderer.invoke("app:release-install"),
|
|
414
|
+
onState: (callback) => {
|
|
415
|
+
const fn = (_event, snapshot) => callback(snapshot);
|
|
416
|
+
import_electron.ipcRenderer.on("app-update-state", fn);
|
|
417
|
+
return () => import_electron.ipcRenderer.removeListener("app-update-state", fn);
|
|
418
|
+
}
|
|
409
419
|
}
|
|
410
420
|
};
|
|
411
421
|
import_electron.contextBridge.exposeInMainWorld("electron", { ...electronAPI, ...packContributions });
|
package/dist/preload.js
CHANGED
|
@@ -427,6 +427,16 @@ export function buildPreloadApi(packContributions = {}) {
|
|
|
427
427
|
return () => ipcRenderer.removeListener('terminal:spawned', fn);
|
|
428
428
|
},
|
|
429
429
|
},
|
|
430
|
+
app: {
|
|
431
|
+
snapshot: () => ipcRenderer.invoke('app:release-snapshot'),
|
|
432
|
+
check: () => ipcRenderer.invoke('app:release-check'),
|
|
433
|
+
install: () => ipcRenderer.invoke('app:release-install'),
|
|
434
|
+
onState: (callback) => {
|
|
435
|
+
const fn = (_event, snapshot) => callback(snapshot);
|
|
436
|
+
ipcRenderer.on('app-update-state', fn);
|
|
437
|
+
return () => ipcRenderer.removeListener('app-update-state', fn);
|
|
438
|
+
},
|
|
439
|
+
},
|
|
430
440
|
};
|
|
431
441
|
contextBridge.exposeInMainWorld('electron', { ...electronAPI, ...packContributions });
|
|
432
442
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `present_files`:模型声明本轮的最终交付文件。
|
|
3
|
+
*
|
|
4
|
+
* 回合产物列表(turn-files)把声明过的文件渲染成交付卡片,其余本轮写过的
|
|
5
|
+
* 文件归入「Edited N files」。只看扩展名分不清交付物与检查用的预览图、
|
|
6
|
+
* 导出副本,所以由模型显式声明。工具只 stat 元数据、不读内容,也不复制
|
|
7
|
+
* 文件——用户打开的是源文件当前版本。
|
|
8
|
+
*/
|
|
9
|
+
export declare const PRESENT_FILES_TOOL_NAME = "present_files";
|
|
10
|
+
export declare const PRESENT_FILES_SCHEMA: {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
mode: "read";
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: string;
|
|
16
|
+
properties: {
|
|
17
|
+
files: {
|
|
18
|
+
type: string;
|
|
19
|
+
minItems: number;
|
|
20
|
+
maxItems: number;
|
|
21
|
+
description: string;
|
|
22
|
+
items: {
|
|
23
|
+
type: string;
|
|
24
|
+
properties: {
|
|
25
|
+
path: {
|
|
26
|
+
type: string;
|
|
27
|
+
description: string;
|
|
28
|
+
};
|
|
29
|
+
description: {
|
|
30
|
+
type: string;
|
|
31
|
+
description: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
required: string[];
|
|
35
|
+
additionalProperties: boolean;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
required: string[];
|
|
40
|
+
additionalProperties: boolean;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
export interface PresentedFile {
|
|
44
|
+
path: string;
|
|
45
|
+
description?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 把参数里的路径解析成绝对路径:展开 `~/`,相对路径按项目根解析;
|
|
49
|
+
* 无项目根的相对路径无法定位,返回 null。
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolvePresentedPath(raw: string, projectRoot: string | null, homeDir?: string): string | null;
|
|
52
|
+
/** 从工具参数里取出声明的文件(不校验存在性);形状不符的条目跳过。 */
|
|
53
|
+
export declare function readPresentedArgs(args: unknown, projectRoot: string | null, homeDir?: string): PresentedFile[];
|
|
54
|
+
/**
|
|
55
|
+
* 执行 `present_files`:每个路径必须是已存在的普通文件。任一路径不合格时
|
|
56
|
+
* 整次调用失败并逐个说明原因,模型可以修正后重试。
|
|
57
|
+
*/
|
|
58
|
+
export declare function executePresentFiles(args: Record<string, unknown>, projectRoot: string | null): Promise<{
|
|
59
|
+
success: true;
|
|
60
|
+
presented: PresentedFile[];
|
|
61
|
+
} | {
|
|
62
|
+
success: false;
|
|
63
|
+
error: string;
|
|
64
|
+
}>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `present_files`:模型声明本轮的最终交付文件。
|
|
3
|
+
*
|
|
4
|
+
* 回合产物列表(turn-files)把声明过的文件渲染成交付卡片,其余本轮写过的
|
|
5
|
+
* 文件归入「Edited N files」。只看扩展名分不清交付物与检查用的预览图、
|
|
6
|
+
* 导出副本,所以由模型显式声明。工具只 stat 元数据、不读内容,也不复制
|
|
7
|
+
* 文件——用户打开的是源文件当前版本。
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs/promises';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
export const PRESENT_FILES_TOOL_NAME = 'present_files';
|
|
13
|
+
/** 单次调用最多声明的文件数;交付卡片过多就失去「最终结果」的意义。 */
|
|
14
|
+
const MAX_PRESENTED_FILES = 4;
|
|
15
|
+
export const PRESENT_FILES_SCHEMA = {
|
|
16
|
+
name: PRESENT_FILES_TOOL_NAME,
|
|
17
|
+
description: 'Declare existing local files as the final deliverables of this turn. ' +
|
|
18
|
+
'When a file you created or updated is an output the user asked to receive ' +
|
|
19
|
+
'(spreadsheet, slide deck, document, report, image, exported archive), call this after writing it ' +
|
|
20
|
+
'and before your final reply, including files produced by scripts or commands. ' +
|
|
21
|
+
'Do not present helper scripts, scratch files, render previews made only to check your work, ' +
|
|
22
|
+
'or intermediate exports. Files you write but do not present are still listed to the user as edited files.',
|
|
23
|
+
mode: 'read',
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
files: {
|
|
28
|
+
type: 'array',
|
|
29
|
+
minItems: 1,
|
|
30
|
+
maxItems: MAX_PRESENTED_FILES,
|
|
31
|
+
description: `Usually the 1-2 most important deliverables; at most ${MAX_PRESENTED_FILES}.`,
|
|
32
|
+
items: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
path: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'Absolute path, or a path relative to the project root.',
|
|
38
|
+
},
|
|
39
|
+
description: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
description: 'Optional one-line summary shown on the file card.',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
required: ['path'],
|
|
45
|
+
additionalProperties: false,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
required: ['files'],
|
|
50
|
+
additionalProperties: false,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* 把参数里的路径解析成绝对路径:展开 `~/`,相对路径按项目根解析;
|
|
55
|
+
* 无项目根的相对路径无法定位,返回 null。
|
|
56
|
+
*/
|
|
57
|
+
export function resolvePresentedPath(raw, projectRoot, homeDir = os.homedir()) {
|
|
58
|
+
const trimmed = raw.trim();
|
|
59
|
+
if (!trimmed)
|
|
60
|
+
return null;
|
|
61
|
+
const expanded = trimmed === '~' || trimmed.startsWith('~/')
|
|
62
|
+
? path.join(homeDir, trimmed.slice(1))
|
|
63
|
+
: trimmed;
|
|
64
|
+
if (path.isAbsolute(expanded))
|
|
65
|
+
return path.normalize(expanded);
|
|
66
|
+
return projectRoot ? path.resolve(projectRoot, expanded) : null;
|
|
67
|
+
}
|
|
68
|
+
/** 从工具参数里取出声明的文件(不校验存在性);形状不符的条目跳过。 */
|
|
69
|
+
export function readPresentedArgs(args, projectRoot, homeDir) {
|
|
70
|
+
if (!args || typeof args !== 'object')
|
|
71
|
+
return [];
|
|
72
|
+
const files = args.files;
|
|
73
|
+
if (!Array.isArray(files))
|
|
74
|
+
return [];
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const item of files) {
|
|
77
|
+
if (!item || typeof item !== 'object')
|
|
78
|
+
continue;
|
|
79
|
+
const record = item;
|
|
80
|
+
if (typeof record.path !== 'string')
|
|
81
|
+
continue;
|
|
82
|
+
const resolved = resolvePresentedPath(record.path, projectRoot, homeDir);
|
|
83
|
+
if (!resolved)
|
|
84
|
+
continue;
|
|
85
|
+
const description = typeof record.description === 'string' ? record.description.trim() : '';
|
|
86
|
+
out.push({ path: resolved, ...(description ? { description } : {}) });
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 执行 `present_files`:每个路径必须是已存在的普通文件。任一路径不合格时
|
|
92
|
+
* 整次调用失败并逐个说明原因,模型可以修正后重试。
|
|
93
|
+
*/
|
|
94
|
+
export async function executePresentFiles(args, projectRoot) {
|
|
95
|
+
const rawFiles = Array.isArray(args.files) ? args.files : [];
|
|
96
|
+
if (rawFiles.length === 0) {
|
|
97
|
+
return { success: false, error: 'files must list at least one file to present.' };
|
|
98
|
+
}
|
|
99
|
+
if (rawFiles.length > MAX_PRESENTED_FILES) {
|
|
100
|
+
return {
|
|
101
|
+
success: false,
|
|
102
|
+
error: `Present at most ${MAX_PRESENTED_FILES} files per call; keep only the final deliverables.`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const problems = [];
|
|
106
|
+
const presented = [];
|
|
107
|
+
for (const item of rawFiles) {
|
|
108
|
+
const raw = item && typeof item === 'object' && typeof item.path === 'string'
|
|
109
|
+
? item.path
|
|
110
|
+
: '';
|
|
111
|
+
const [file] = readPresentedArgs({ files: [item] }, projectRoot);
|
|
112
|
+
if (!file) {
|
|
113
|
+
problems.push(raw
|
|
114
|
+
? `${raw}: use an absolute path (no project root to resolve a relative path).`
|
|
115
|
+
: 'each entry needs a non-empty path.');
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const stat = await fs.stat(file.path);
|
|
120
|
+
if (!stat.isFile()) {
|
|
121
|
+
problems.push(`${file.path}: not a regular file.`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// stat 失败(多为 ENOENT):报给模型让它先写文件或修正路径。
|
|
127
|
+
problems.push(`${file.path}: file not found. Create it first or fix the path.`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
presented.push(file);
|
|
131
|
+
}
|
|
132
|
+
if (problems.length > 0) {
|
|
133
|
+
return { success: false, error: `Cannot present: ${problems.join(' ')}` };
|
|
134
|
+
}
|
|
135
|
+
return { success: true, presented };
|
|
136
|
+
}
|
package/dist/product-config.d.ts
CHANGED
|
@@ -66,6 +66,10 @@ export interface ProductConfig {
|
|
|
66
66
|
* 设置入口。缺省全开。`false` 藏对应侧栏页或综合设置分段。
|
|
67
67
|
*/
|
|
68
68
|
settings?: Record<string, boolean>;
|
|
69
|
+
/**
|
|
70
|
+
* 对话与配置的导出/导入。缺省关。只有显式 `true` 的产品才有入口和接口。
|
|
71
|
+
*/
|
|
72
|
+
portable?: boolean;
|
|
69
73
|
/**
|
|
70
74
|
* 产品钉死的大模型。`settings.llm === false` 时必填,运行时用这份,
|
|
71
75
|
* 不再读设置页。密钥用 `apiKeyEnv` 指向环境变量,不要把 key 写进仓库。
|
|
@@ -105,6 +109,8 @@ export interface ProductConfig {
|
|
|
105
109
|
}
|
|
106
110
|
/** shell 内置智能体:只有产品显式 `true` 才开。 */
|
|
107
111
|
export declare function isShellBuiltinAgentEnabled(id: 'local-assistant' | 'all-round-assistant', config?: ProductConfig): boolean;
|
|
112
|
+
/** 对话与配置能否导出/导入。缺省关,产品必须显式打开。 */
|
|
113
|
+
export declare function isPortableProduct(config?: ProductConfig): boolean;
|
|
108
114
|
/** shell 内置技能:`true` 全开;否则只有对象里显式 `true` 的目录开。 */
|
|
109
115
|
export declare function isShellBuiltinSkillEnabled(id: string, config?: ProductConfig): boolean;
|
|
110
116
|
/**
|
package/dist/product-config.js
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
export function isShellBuiltinAgentEnabled(id, config = getProductConfig()) {
|
|
14
14
|
return config.builtinAgents?.[id] === true;
|
|
15
15
|
}
|
|
16
|
+
/** 对话与配置能否导出/导入。缺省关,产品必须显式打开。 */
|
|
17
|
+
export function isPortableProduct(config = getProductConfig()) {
|
|
18
|
+
return config.portable === true;
|
|
19
|
+
}
|
|
16
20
|
/** shell 内置技能:`true` 全开;否则只有对象里显式 `true` 的目录开。 */
|
|
17
21
|
export function isShellBuiltinSkillEnabled(id, config = getProductConfig()) {
|
|
18
22
|
const value = config.builtinSkills;
|
package/dist/server/start.js
CHANGED
|
@@ -92,7 +92,7 @@ export async function startBsHost(options = {}) {
|
|
|
92
92
|
shutdown() {
|
|
93
93
|
shutdownPromise ??= (async () => {
|
|
94
94
|
try {
|
|
95
|
-
|
|
95
|
+
const closed = new Promise((resolve, reject) => {
|
|
96
96
|
server.close((error) => {
|
|
97
97
|
if (error)
|
|
98
98
|
reject(error);
|
|
@@ -100,6 +100,12 @@ export async function startBsHost(options = {}) {
|
|
|
100
100
|
resolve();
|
|
101
101
|
});
|
|
102
102
|
});
|
|
103
|
+
// The Tauri webview keeps HTTP/SSE connections alive while its
|
|
104
|
+
// ExitRequested handler waits for this promise. Force those
|
|
105
|
+
// connections closed so Cmd+Q does not deadlock until the outer
|
|
106
|
+
// five-second process timeout.
|
|
107
|
+
server.closeAllConnections();
|
|
108
|
+
await closed;
|
|
103
109
|
}
|
|
104
110
|
finally {
|
|
105
111
|
await runtime.shutdown();
|
package/dist/sidecar/boot.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SidecarSupervisor } from './supervisor.js';
|
|
1
2
|
import type { ScopedStore } from '../storage/scoped-store.js';
|
|
2
3
|
import { type ToolRouter } from '../tool-router.js';
|
|
3
4
|
import type { createApprovalBridge } from './reverse-approval.js';
|
|
@@ -28,10 +29,24 @@ export interface HostSidecarDeps {
|
|
|
28
29
|
onLogLine?: (line: string) => void;
|
|
29
30
|
}
|
|
30
31
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* (
|
|
32
|
+
* Wire the sidecar plugin registry into the host tool router.
|
|
33
|
+
*
|
|
34
|
+
* The `plugin.list` probe doubles as the availability check: on failure the
|
|
35
|
+
* router is cleared (no plugin_* tools, no plugin-provided tools) and the
|
|
36
|
+
* error is rethrown for the caller to log. On success the router gets the
|
|
37
|
+
* plugin.* RPC seam plus a `tool.invoke` forwarder, then syncs the enabled
|
|
38
|
+
* plugins' tool descriptors from `plugin.tools.describe`. Plugin tools run
|
|
39
|
+
* only in the sidecar; approval already happened in its CoreLoop before a
|
|
40
|
+
* call is forwarded here, hence `consentGranted`.
|
|
41
|
+
*
|
|
42
|
+
* @param supervisor Running sidecar supervisor.
|
|
43
|
+
* @param toolRouter Host router that advertises and dispatches the tools.
|
|
44
|
+
*/
|
|
45
|
+
export declare function wirePluginRegistry(supervisor: Pick<SidecarSupervisor, 'call' | 'invokeTool'>, toolRouter: ToolRouter): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* The complete Python sidecar hosts the Rust CoreLoop and is mandatory.
|
|
48
|
+
* Boot is registered synchronously so early RPC thin clients (skill-loader
|
|
49
|
+
* et al.) can await it via whenSidecarSupervisor().
|
|
35
50
|
*/
|
|
36
51
|
export declare function startHostSidecar(deps: HostSidecarDeps): Promise<void>;
|
|
37
52
|
/** 宿主退出钩子:停 supervisor + egress proxy。幂等。 */
|