@steerable/agent-shell 0.6.74 → 0.7.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/dist/fence-path.d.ts +16 -0
- package/dist/fence-path.js +54 -0
- package/dist/host/runtime.d.ts +0 -13
- package/dist/host/runtime.js +26 -0
- package/dist/host-tools.d.ts +1 -0
- package/dist/host-tools.js +1 -0
- package/dist/i18n.d.ts +2 -0
- package/dist/i18n.js +6 -1
- package/dist/json-store.d.ts +1 -0
- package/dist/json-store.js +50 -4
- package/dist/local-backend/coreloop-stream.d.ts +29 -5
- package/dist/local-backend/coreloop-stream.js +122 -1
- package/dist/local-backend/fork-copy.d.ts +9 -0
- package/dist/local-backend/fork-copy.js +75 -0
- package/dist/local-backend/history-edit.d.ts +12 -2
- package/dist/local-backend/history-edit.js +98 -4
- package/dist/local-backend/router.d.ts +1 -0
- package/dist/local-backend/router.js +189 -53
- package/dist/local-backend/task-process.js +5 -0
- package/dist/local-backend/task-service.js +6 -7
- package/dist/local-backend/turn-files.js +3 -1
- package/dist/local-executor.d.ts +1 -3
- package/dist/local-executor.js +10 -13
- package/dist/product-config.d.ts +7 -1
- package/dist/project-registry.d.ts +33 -0
- package/dist/project-registry.js +206 -78
- package/dist/project-rules.d.ts +2 -0
- package/dist/project-rules.js +17 -13
- package/dist/scenario/pack.d.ts +1 -1
- package/dist/sidecar/approval-label.d.ts +55 -0
- package/dist/sidecar/approval-label.js +102 -0
- package/dist/sidecar/reverse-approval.d.ts +26 -0
- package/dist/sidecar/reverse-approval.js +15 -4
- package/dist/sidecar/types.d.ts +14 -0
- package/dist/storage/index.d.ts +8 -1
- package/dist/storage/index.js +7 -2
- package/dist/storage/process-locks.d.ts +6 -0
- package/dist/storage/process-locks.js +6 -0
- package/dist/storage/scoped-store.d.ts +1 -1
- package/dist/storage/sqlite-driver.js +6 -5
- package/dist/tool-router.d.ts +22 -9
- package/dist/tool-router.js +43 -29
- package/package.json +4 -4
package/dist/local-executor.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createHash } from 'crypto';
|
|
|
6
6
|
import { spawn, execSync } from 'child_process';
|
|
7
7
|
import { log } from './log.js';
|
|
8
8
|
import { constants as fsConstants } from 'fs';
|
|
9
|
+
import { expandFencePath, resolveFencePath } from './fence-path.js';
|
|
9
10
|
import { adaptCommandForPowerShell } from './shell-adapt.js';
|
|
10
11
|
import { evaluateExecPolicy, planSshArgv, } from './shell-exec.js';
|
|
11
12
|
import { nodeShellBackend } from './shell-process.js';
|
|
@@ -181,15 +182,11 @@ export function isGuiLaunchCommand(command) {
|
|
|
181
182
|
*/
|
|
182
183
|
export function isPathWithinRoot(resolvedPath, resolvedRoot) {
|
|
183
184
|
const rel = path.relative(resolvedRoot, resolvedPath);
|
|
184
|
-
return rel === '' || (
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export function expandFencePath(inputPath) {
|
|
188
|
-
const expanded = inputPath.startsWith('~')
|
|
189
|
-
? path.join(os.homedir(), inputPath.slice(1))
|
|
190
|
-
: inputPath;
|
|
191
|
-
return path.resolve(expanded);
|
|
185
|
+
return rel === '' || (rel !== '..' &&
|
|
186
|
+
!rel.startsWith(`..${path.sep}`) &&
|
|
187
|
+
!path.isAbsolute(rel));
|
|
192
188
|
}
|
|
189
|
+
export { canonicalFencePath, expandFencePath, resolveFencePath } from './fence-path.js';
|
|
193
190
|
/**
|
|
194
191
|
* 可写根:项目家目录(或对话工作区)加上额外可写根(源文件夹)。
|
|
195
192
|
* 每个根都包含它自己和全部子目录。空列表表示不设围栏。
|
|
@@ -639,7 +636,7 @@ export class LocalExecutor {
|
|
|
639
636
|
}
|
|
640
637
|
async readLocalFile(request, projectRoot, additionalReadRoots) {
|
|
641
638
|
try {
|
|
642
|
-
const filePath = this.resolvePath(request.path);
|
|
639
|
+
const filePath = this.resolvePath(request.path, projectRoot);
|
|
643
640
|
if (projectRoot) {
|
|
644
641
|
const root = this.resolvePath(projectRoot);
|
|
645
642
|
const inAdditionalRoot = (additionalReadRoots ?? []).some((r) => isPathWithinRoot(filePath, this.resolvePath(r)));
|
|
@@ -674,7 +671,7 @@ export class LocalExecutor {
|
|
|
674
671
|
}
|
|
675
672
|
}
|
|
676
673
|
async writeLocalFile(request, projectRoot, additionalWriteRoots) {
|
|
677
|
-
const filePath = this.resolvePath(request.path);
|
|
674
|
+
const filePath = this.resolvePath(request.path, projectRoot);
|
|
678
675
|
const violation = writableFenceViolation(filePath, writableFenceRoots(projectRoot, additionalWriteRoots));
|
|
679
676
|
if (violation)
|
|
680
677
|
return { success: false, error: violation };
|
|
@@ -735,7 +732,7 @@ export class LocalExecutor {
|
|
|
735
732
|
* 原子落盘。返回新版本令牌与统一 diff。
|
|
736
733
|
*/
|
|
737
734
|
async editLocalFile(request, projectRoot, additionalWriteRoots) {
|
|
738
|
-
const filePath = this.resolvePath(request.path);
|
|
735
|
+
const filePath = this.resolvePath(request.path, projectRoot);
|
|
739
736
|
const violation = writableFenceViolation(filePath, writableFenceRoots(projectRoot, additionalWriteRoots));
|
|
740
737
|
if (violation)
|
|
741
738
|
return { success: false, error: violation };
|
|
@@ -1096,8 +1093,8 @@ export class LocalExecutor {
|
|
|
1096
1093
|
return new RegExp(d.pattern, flags);
|
|
1097
1094
|
});
|
|
1098
1095
|
}
|
|
1099
|
-
resolvePath(inputPath) {
|
|
1100
|
-
return
|
|
1096
|
+
resolvePath(inputPath, baseDir) {
|
|
1097
|
+
return resolveFencePath(inputPath, baseDir);
|
|
1101
1098
|
}
|
|
1102
1099
|
resolveCwd(cwd, shellType) {
|
|
1103
1100
|
const resolved = this.resolvePath(cwd || os.homedir());
|
package/dist/product-config.d.ts
CHANGED
|
@@ -43,7 +43,8 @@ export interface ProductConfig {
|
|
|
43
43
|
}>;
|
|
44
44
|
/**
|
|
45
45
|
* 命令安全询问:host = 弹宿主审批;off = 本轮不挂审批。缺省 host。
|
|
46
|
-
* STEERABLE_APPROVAL=0
|
|
46
|
+
* STEERABLE_APPROVAL=0 关掉本轮审批层,破坏性工具由工具路由拒绝。
|
|
47
|
+
* 需要放行时用命令行 `--approve allow-all`,不要依赖这个环境变量。
|
|
47
48
|
*/
|
|
48
49
|
approval?: 'host' | 'off';
|
|
49
50
|
/**
|
|
@@ -126,6 +127,11 @@ export interface ProductConfig {
|
|
|
126
127
|
locales: string[];
|
|
127
128
|
defaultLocale: string;
|
|
128
129
|
};
|
|
130
|
+
/**
|
|
131
|
+
* 无项目会话分叉时,工作区复制的字节上限。缺省 500 MiB。
|
|
132
|
+
* 超过则接口返回 413,不创建新会话。
|
|
133
|
+
*/
|
|
134
|
+
forkWorkspaceMaxBytes?: number;
|
|
129
135
|
}
|
|
130
136
|
/** shell 内置智能体:只有产品显式 `true` 才开。 */
|
|
131
137
|
export declare function isShellBuiltinAgentEnabled(id: 'local-assistant' | 'all-round-assistant', config?: ProductConfig): boolean;
|
|
@@ -30,6 +30,11 @@ export interface ProjectRecord {
|
|
|
30
30
|
* 旧记录没有此字段,读取时按 `false` 处理(见 {@link ProjectRegistry.isTrusted})。
|
|
31
31
|
*/
|
|
32
32
|
trusted?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* 命令行创建的项目。桌面创建的记录不写该字段。
|
|
35
|
+
* 旧记录没有此字段,读取时忽略。
|
|
36
|
+
*/
|
|
37
|
+
origin?: 'cli';
|
|
33
38
|
/**
|
|
34
39
|
* 侧边栏显示顺序,越小越靠前。旧记录没有此字段时按 createdAt 升序。
|
|
35
40
|
* 用户拖拽排序后每个项目都会写上序号;之后新建的项目没有序号,排在已编号项目之后。
|
|
@@ -42,11 +47,17 @@ export interface CreateProjectInput {
|
|
|
42
47
|
name: string;
|
|
43
48
|
folderPath: string;
|
|
44
49
|
sourceFolders?: string[];
|
|
50
|
+
origin?: 'cli';
|
|
45
51
|
}
|
|
46
52
|
/** 最小 KV 存储接口,避免本模块直接依赖具体存储实现。 */
|
|
47
53
|
export interface ProjectKvStore {
|
|
48
54
|
get(key: 'projects'): ProjectRecord[] | undefined;
|
|
49
55
|
set(key: 'projects', value: ProjectRecord[]): void;
|
|
56
|
+
/**
|
|
57
|
+
* Read-modify-write under the projects file lock. Absent in memory stores;
|
|
58
|
+
* the registry then updates in this process only.
|
|
59
|
+
*/
|
|
60
|
+
mutate?(key: 'projects', update: (current: ProjectRecord[]) => ProjectRecord[]): ProjectRecord[];
|
|
50
61
|
}
|
|
51
62
|
export declare class ProjectRegistry {
|
|
52
63
|
private readonly store;
|
|
@@ -54,6 +65,27 @@ export declare class ProjectRegistry {
|
|
|
54
65
|
list(): ProjectRecord[];
|
|
55
66
|
get(idOrName: string): ProjectRecord | null;
|
|
56
67
|
create(input: CreateProjectInput): ProjectRecord;
|
|
68
|
+
/**
|
|
69
|
+
* 同名时在名称后加 `-2`、`-3`,与家目录重名的处理一致。
|
|
70
|
+
*/
|
|
71
|
+
createUnique(input: CreateProjectInput): ProjectRecord;
|
|
72
|
+
/**
|
|
73
|
+
* 按真实路径查找。目录已被删除时 `realpath` 失败,改用记录里的原始路径。
|
|
74
|
+
* Windows 上只忽略大小写。
|
|
75
|
+
*/
|
|
76
|
+
findByFolder(realDir: string, realpath?: (target: string) => string, platform?: NodeJS.Platform): ProjectRecord | null;
|
|
77
|
+
/**
|
|
78
|
+
* 在同一次读改写里查找或创建。`D` 等于已有项目,或落在某个项目目录之内时复用
|
|
79
|
+
* (取路径最长的那个);否则以目录名创建,重名加后缀。
|
|
80
|
+
*/
|
|
81
|
+
resolveByFolder(folderPath: string, options?: {
|
|
82
|
+
origin?: 'cli';
|
|
83
|
+
realpath?: (target: string) => string;
|
|
84
|
+
platform?: NodeJS.Platform;
|
|
85
|
+
}): {
|
|
86
|
+
project: ProjectRecord;
|
|
87
|
+
created: boolean;
|
|
88
|
+
};
|
|
57
89
|
update(id: string, updates: Partial<CreateProjectInput>): ProjectRecord;
|
|
58
90
|
delete(id: string): boolean;
|
|
59
91
|
/**
|
|
@@ -69,4 +101,5 @@ export declare class ProjectRegistry {
|
|
|
69
101
|
isTrusted(id: string): boolean;
|
|
70
102
|
/** W6-5: grant or revoke trust. Returns the updated record. */
|
|
71
103
|
setTrusted(id: string, trusted: boolean): ProjectRecord;
|
|
104
|
+
private rewrite;
|
|
72
105
|
}
|
package/dist/project-registry.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* mcp-server-registry.ts 同一模式)。
|
|
13
13
|
*/
|
|
14
14
|
import { randomUUID } from 'node:crypto';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
15
17
|
const STORE_KEY = 'projects';
|
|
16
18
|
export class ProjectRegistry {
|
|
17
19
|
store;
|
|
@@ -34,85 +36,141 @@ export class ProjectRegistry {
|
|
|
34
36
|
throw new Error('项目名称不能为空');
|
|
35
37
|
if (!folderPath)
|
|
36
38
|
throw new Error('项目文件夹不能为空');
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
createdAt: now,
|
|
47
|
-
updatedAt: now,
|
|
48
|
-
};
|
|
49
|
-
this.store.set(STORE_KEY, [...this.list(), entry]);
|
|
50
|
-
return entry;
|
|
39
|
+
let created = null;
|
|
40
|
+
this.rewrite((projects) => {
|
|
41
|
+
if (projects.some((project) => project.name.toLowerCase() === name.toLowerCase())) {
|
|
42
|
+
throw new Error(`已存在同名项目「${name}」`);
|
|
43
|
+
}
|
|
44
|
+
created = buildProject(name, folderPath, input);
|
|
45
|
+
return [...projects, created];
|
|
46
|
+
});
|
|
47
|
+
return created;
|
|
51
48
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
if (!nextName)
|
|
49
|
+
/**
|
|
50
|
+
* 同名时在名称后加 `-2`、`-3`,与家目录重名的处理一致。
|
|
51
|
+
*/
|
|
52
|
+
createUnique(input) {
|
|
53
|
+
const base = input.name.trim();
|
|
54
|
+
const folderPath = input.folderPath.trim();
|
|
55
|
+
if (!base)
|
|
60
56
|
throw new Error('项目名称不能为空');
|
|
61
|
-
|
|
62
|
-
if (nameClash)
|
|
63
|
-
throw new Error(`已存在同名项目「${nextName}」`);
|
|
64
|
-
const nextFolder = updates.folderPath !== undefined
|
|
65
|
-
? updates.folderPath.trim()
|
|
66
|
-
: current.folderPath;
|
|
67
|
-
if (!nextFolder)
|
|
57
|
+
if (!folderPath)
|
|
68
58
|
throw new Error('项目文件夹不能为空');
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
...
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
59
|
+
let created = null;
|
|
60
|
+
this.rewrite((projects) => {
|
|
61
|
+
const name = uniqueProjectName(projects, base);
|
|
62
|
+
created = buildProject(name, folderPath, input);
|
|
63
|
+
return [...projects, created];
|
|
64
|
+
});
|
|
65
|
+
return created;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 按真实路径查找。目录已被删除时 `realpath` 失败,改用记录里的原始路径。
|
|
69
|
+
* Windows 上只忽略大小写。
|
|
70
|
+
*/
|
|
71
|
+
findByFolder(realDir, realpath = fs.realpathSync, platform = process.platform) {
|
|
72
|
+
const target = canonicalFolder(realDir, realpath, platform);
|
|
73
|
+
if (!target)
|
|
74
|
+
return null;
|
|
75
|
+
return locateProject(this.list(), target, realpath, platform);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 在同一次读改写里查找或创建。`D` 等于已有项目,或落在某个项目目录之内时复用
|
|
79
|
+
* (取路径最长的那个);否则以目录名创建,重名加后缀。
|
|
80
|
+
*/
|
|
81
|
+
resolveByFolder(folderPath, options) {
|
|
82
|
+
const realpath = options?.realpath ?? fs.realpathSync;
|
|
83
|
+
const platform = options?.platform ?? process.platform;
|
|
84
|
+
const target = canonicalFolder(folderPath, realpath, platform);
|
|
85
|
+
if (!target)
|
|
86
|
+
throw new Error('项目文件夹不能为空');
|
|
87
|
+
let created = false;
|
|
88
|
+
let chosen = null;
|
|
89
|
+
this.rewrite((projects) => {
|
|
90
|
+
const existing = locateProject(projects, target, realpath, platform);
|
|
91
|
+
if (existing) {
|
|
92
|
+
chosen = existing;
|
|
93
|
+
return projects;
|
|
94
|
+
}
|
|
95
|
+
const name = uniqueProjectName(projects, pathFor(platform).basename(target) || 'project');
|
|
96
|
+
chosen = buildProject(name, target, { name, folderPath: target, origin: options?.origin });
|
|
97
|
+
created = true;
|
|
98
|
+
return [...projects, chosen];
|
|
99
|
+
});
|
|
100
|
+
return { project: chosen, created };
|
|
101
|
+
}
|
|
102
|
+
update(id, updates) {
|
|
103
|
+
let updated = null;
|
|
104
|
+
this.rewrite((projects) => {
|
|
105
|
+
const idx = projects.findIndex((p) => p.id === id);
|
|
106
|
+
if (idx === -1)
|
|
107
|
+
throw new Error('项目不存在');
|
|
108
|
+
const current = projects[idx];
|
|
109
|
+
const nextName = updates.name !== undefined ? updates.name.trim() : current.name;
|
|
110
|
+
if (!nextName)
|
|
111
|
+
throw new Error('项目名称不能为空');
|
|
112
|
+
const nameClash = projects.some((p) => p.id !== id && p.name.toLowerCase() === nextName.toLowerCase());
|
|
113
|
+
if (nameClash)
|
|
114
|
+
throw new Error(`已存在同名项目「${nextName}」`);
|
|
115
|
+
const nextFolder = updates.folderPath !== undefined
|
|
116
|
+
? updates.folderPath.trim()
|
|
117
|
+
: current.folderPath;
|
|
118
|
+
if (!nextFolder)
|
|
119
|
+
throw new Error('项目文件夹不能为空');
|
|
120
|
+
const sourceFolders = updates.sourceFolders !== undefined
|
|
121
|
+
? normalizeSourceFolders(updates.sourceFolders, nextFolder)
|
|
122
|
+
: normalizeSourceFolders(current.sourceFolders, nextFolder);
|
|
123
|
+
const next = {
|
|
124
|
+
...current,
|
|
125
|
+
name: nextName,
|
|
126
|
+
folderPath: nextFolder,
|
|
127
|
+
sourceFolders: sourceFolders.length > 0 ? sourceFolders : undefined,
|
|
128
|
+
updatedAt: new Date().toISOString(),
|
|
129
|
+
};
|
|
130
|
+
updated = next;
|
|
131
|
+
const copy = projects.slice();
|
|
132
|
+
copy[idx] = next;
|
|
133
|
+
return copy;
|
|
134
|
+
});
|
|
135
|
+
return updated;
|
|
82
136
|
}
|
|
83
137
|
delete(id) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
138
|
+
let removed = false;
|
|
139
|
+
this.rewrite((projects) => {
|
|
140
|
+
const next = projects.filter((p) => p.id !== id);
|
|
141
|
+
removed = next.length !== projects.length;
|
|
142
|
+
return removed ? next : projects;
|
|
143
|
+
});
|
|
144
|
+
return removed;
|
|
90
145
|
}
|
|
91
146
|
/**
|
|
92
147
|
* 按 orderedIds 重排侧边栏。未出现的 id 忽略;名单里没提到的项目
|
|
93
148
|
* 保持相对顺序接在后面。写入连续的 sortOrder,list() 随后按该序号返回。
|
|
94
149
|
*/
|
|
95
150
|
reorder(orderedIds) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
for (const project of current) {
|
|
108
|
-
if (!seen.has(project.id))
|
|
151
|
+
let stamped = [];
|
|
152
|
+
this.rewrite((projects) => {
|
|
153
|
+
const current = [...projects].sort(compareProjects);
|
|
154
|
+
const byId = new Map(current.map((project) => [project.id, project]));
|
|
155
|
+
const next = [];
|
|
156
|
+
const seen = new Set();
|
|
157
|
+
for (const id of orderedIds) {
|
|
158
|
+
const project = byId.get(id);
|
|
159
|
+
if (!project || seen.has(id))
|
|
160
|
+
continue;
|
|
161
|
+
seen.add(id);
|
|
109
162
|
next.push(project);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
163
|
+
}
|
|
164
|
+
for (const project of current) {
|
|
165
|
+
if (!seen.has(project.id))
|
|
166
|
+
next.push(project);
|
|
167
|
+
}
|
|
168
|
+
stamped = next.map((project, index) => ({
|
|
169
|
+
...project,
|
|
170
|
+
sortOrder: index,
|
|
171
|
+
}));
|
|
172
|
+
return stamped;
|
|
173
|
+
});
|
|
116
174
|
return stamped;
|
|
117
175
|
}
|
|
118
176
|
/**
|
|
@@ -125,17 +183,28 @@ export class ProjectRegistry {
|
|
|
125
183
|
}
|
|
126
184
|
/** W6-5: grant or revoke trust. Returns the updated record. */
|
|
127
185
|
setTrusted(id, trusted) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
186
|
+
let updated = null;
|
|
187
|
+
this.rewrite((projects) => {
|
|
188
|
+
const idx = projects.findIndex((p) => p.id === id);
|
|
189
|
+
if (idx === -1)
|
|
190
|
+
throw new Error('项目不存在');
|
|
191
|
+
const next = {
|
|
192
|
+
...projects[idx],
|
|
193
|
+
trusted,
|
|
194
|
+
updatedAt: new Date().toISOString(),
|
|
195
|
+
};
|
|
196
|
+
updated = next;
|
|
197
|
+
const copy = projects.slice();
|
|
198
|
+
copy[idx] = next;
|
|
199
|
+
return copy;
|
|
200
|
+
});
|
|
201
|
+
return updated;
|
|
202
|
+
}
|
|
203
|
+
rewrite(update) {
|
|
204
|
+
if (this.store.mutate)
|
|
205
|
+
return this.store.mutate(STORE_KEY, update);
|
|
206
|
+
const next = update([...(this.store.get(STORE_KEY) ?? [])]);
|
|
207
|
+
this.store.set(STORE_KEY, next);
|
|
139
208
|
return next;
|
|
140
209
|
}
|
|
141
210
|
}
|
|
@@ -167,3 +236,62 @@ function normalizeSourceFolders(folders, homePath) {
|
|
|
167
236
|
}
|
|
168
237
|
return out;
|
|
169
238
|
}
|
|
239
|
+
function buildProject(name, folderPath, input) {
|
|
240
|
+
const sourceFolders = normalizeSourceFolders(input.sourceFolders, folderPath);
|
|
241
|
+
const now = new Date().toISOString();
|
|
242
|
+
return {
|
|
243
|
+
id: randomUUID(),
|
|
244
|
+
name,
|
|
245
|
+
folderPath,
|
|
246
|
+
...(sourceFolders.length > 0 ? { sourceFolders } : {}),
|
|
247
|
+
...(input.origin === 'cli' ? { origin: 'cli' } : {}),
|
|
248
|
+
createdAt: now,
|
|
249
|
+
updatedAt: now,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function uniqueProjectName(projects, base) {
|
|
253
|
+
const taken = new Set(projects.map((project) => project.name.toLowerCase()));
|
|
254
|
+
if (!taken.has(base.toLowerCase()))
|
|
255
|
+
return base;
|
|
256
|
+
for (let index = 2; index < 1000; index += 1) {
|
|
257
|
+
const candidate = `${base}-${index}`;
|
|
258
|
+
if (!taken.has(candidate.toLowerCase()))
|
|
259
|
+
return candidate;
|
|
260
|
+
}
|
|
261
|
+
throw new Error('无法分配项目名称:重名过多');
|
|
262
|
+
}
|
|
263
|
+
function pathFor(platform) {
|
|
264
|
+
return platform === 'win32' ? path.win32 : path.posix;
|
|
265
|
+
}
|
|
266
|
+
function canonicalFolder(folderPath, realpath, platform) {
|
|
267
|
+
const absolute = pathFor(platform).resolve(folderPath.trim());
|
|
268
|
+
if (!absolute)
|
|
269
|
+
return '';
|
|
270
|
+
try {
|
|
271
|
+
return realpath(absolute);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return absolute;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function locateProject(projects, target, realpath, platform) {
|
|
278
|
+
const platformPath = pathFor(platform);
|
|
279
|
+
const keyed = projects.map((project) => ({
|
|
280
|
+
project,
|
|
281
|
+
key: canonicalFolder(project.folderPath, realpath, platform),
|
|
282
|
+
}));
|
|
283
|
+
const exact = keyed.find((item) => platformPath.relative(item.key, target) === '');
|
|
284
|
+
if (exact)
|
|
285
|
+
return exact.project;
|
|
286
|
+
const parents = keyed
|
|
287
|
+
.filter((item) => isInsideFolder(item.key, target, platformPath))
|
|
288
|
+
.sort((left, right) => right.key.length - left.key.length);
|
|
289
|
+
return parents[0]?.project ?? null;
|
|
290
|
+
}
|
|
291
|
+
function isInsideFolder(parent, child, platformPath) {
|
|
292
|
+
const relative = platformPath.relative(parent, child);
|
|
293
|
+
return relative !== ''
|
|
294
|
+
&& relative !== '..'
|
|
295
|
+
&& !relative.startsWith(`..${platformPath.sep}`)
|
|
296
|
+
&& !platformPath.isAbsolute(relative);
|
|
297
|
+
}
|
package/dist/project-rules.d.ts
CHANGED
|
@@ -12,4 +12,6 @@ export interface ProjectRules {
|
|
|
12
12
|
* 向上遍历在「用户 home 目录」处封顶(若项目不在 home 下则按 MAX_LEVELS
|
|
13
13
|
* 封顶)——不扫 `/`、`/etc` 这类与项目无关的祖先目录。
|
|
14
14
|
*/
|
|
15
|
+
/** Rule-file paths only. Does not read file bodies. */
|
|
16
|
+
export declare function listProjectRuleFiles(folderPath: string): string[];
|
|
15
17
|
export declare function loadProjectRuleFiles(folderPath: string): ProjectRules;
|
package/dist/project-rules.js
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* WorldState 的一节),我们此前完全没有。
|
|
8
8
|
*
|
|
9
9
|
* 安全前提(W6-5):这些文件是「项目作者写给 agent 的指令」,属于不可信输入。
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* 本模块只做发现并返回内容。未信任的项目只调用 `listProjectRuleFiles`
|
|
11
|
+
* (stat,不读正文);`loadProjectRuleFiles` 读取正文,仅在已信任时调用。
|
|
12
|
+
* 遍历与注入都有硬上限,单个文件与总量都截断。
|
|
12
13
|
*
|
|
13
14
|
* 「热重载」按回合粒度天然成立:每个回合重新读取磁盘,规则文件保存即生效,
|
|
14
15
|
* 不需要文件 watcher。
|
|
@@ -33,13 +34,13 @@ const EMPTY = { files: [], content: '', truncated: false };
|
|
|
33
34
|
* 向上遍历在「用户 home 目录」处封顶(若项目不在 home 下则按 MAX_LEVELS
|
|
34
35
|
* 封顶)——不扫 `/`、`/etc` 这类与项目无关的祖先目录。
|
|
35
36
|
*/
|
|
36
|
-
|
|
37
|
+
/** Rule-file paths only. Does not read file bodies. */
|
|
38
|
+
export function listProjectRuleFiles(folderPath) {
|
|
37
39
|
if (!folderPath || !folderPath.trim())
|
|
38
|
-
return
|
|
40
|
+
return [];
|
|
39
41
|
const start = path.resolve(folderPath);
|
|
40
42
|
if (!fs.existsSync(start))
|
|
41
|
-
return
|
|
42
|
-
// 构造 根→…→起始目录 的目录链(根在前,越往后越贴近项目)。
|
|
43
|
+
return [];
|
|
43
44
|
const home = os.homedir();
|
|
44
45
|
const chain = [];
|
|
45
46
|
let dir = start;
|
|
@@ -48,23 +49,22 @@ export function loadProjectRuleFiles(folderPath) {
|
|
|
48
49
|
seen.add(dir);
|
|
49
50
|
chain.unshift(dir);
|
|
50
51
|
if (dir === home)
|
|
51
|
-
break;
|
|
52
|
+
break;
|
|
52
53
|
const parent = path.dirname(dir);
|
|
53
54
|
if (parent === dir)
|
|
54
|
-
break;
|
|
55
|
+
break;
|
|
55
56
|
dir = parent;
|
|
56
57
|
}
|
|
57
|
-
// 根→叶收集规则文件;每层内 override 优先。
|
|
58
58
|
const files = [];
|
|
59
59
|
for (const d of chain) {
|
|
60
60
|
for (const name of RULE_FILE_NAMES) {
|
|
61
|
-
const
|
|
61
|
+
const file = path.join(d, name);
|
|
62
62
|
try {
|
|
63
|
-
if (fs.statSync(
|
|
64
|
-
files.push(
|
|
63
|
+
if (fs.statSync(file).isFile())
|
|
64
|
+
files.push(file);
|
|
65
65
|
}
|
|
66
66
|
catch {
|
|
67
|
-
//
|
|
67
|
+
// missing or unreadable
|
|
68
68
|
}
|
|
69
69
|
if (files.length >= MAX_RULE_FILES)
|
|
70
70
|
break;
|
|
@@ -72,6 +72,10 @@ export function loadProjectRuleFiles(folderPath) {
|
|
|
72
72
|
if (files.length >= MAX_RULE_FILES)
|
|
73
73
|
break;
|
|
74
74
|
}
|
|
75
|
+
return files;
|
|
76
|
+
}
|
|
77
|
+
export function loadProjectRuleFiles(folderPath) {
|
|
78
|
+
const files = listProjectRuleFiles(folderPath);
|
|
75
79
|
if (files.length === 0)
|
|
76
80
|
return EMPTY;
|
|
77
81
|
// 拼接并施加总量上限。
|
package/dist/scenario/pack.d.ts
CHANGED
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 契约全文与设计约束见 packages/pack-sdk/types/index.d.ts 头注释。
|
|
7
7
|
*/
|
|
8
|
-
export type { ScenarioId, ToolMode, ToolExposure, PackToolContext, PackToolHandler, ToolContribution, ServiceFactory, ServiceCreateContext, MigrationContribution, SkillContribution, AgentSeedIdentity, AgentSeed, HttpRouteContribution, HttpRouteRequest, MainContribution, ComponentRef, SettingsPanelContribution, RouteContribution, ChatSlotContribution, CardRendererContribution, RendererContribution, BrandSpec, PackagingSpec, CliCommandIO, CliCommandSpec, ScenarioPack, PackBackendRouteResponse, PackBackendRouteRequest, PackBackendRoute, PackTurnObserver, PackTurnHooks, } from '@steerable/pack-sdk';
|
|
8
|
+
export type { ScenarioId, ToolMode, ToolExposure, PackToolContext, PackToolHandler, ToolApprovalSpec, ToolContribution, ServiceFactory, ServiceCreateContext, MigrationContribution, SkillContribution, AgentSeedIdentity, AgentSeed, HttpRouteContribution, HttpRouteRequest, MainContribution, ComponentRef, SettingsPanelContribution, RouteContribution, ChatSlotContribution, CardRendererContribution, RendererContribution, BrandSpec, PackagingSpec, CliCommandIO, CliCommandSpec, ScenarioPack, PackBackendRouteResponse, PackBackendRouteRequest, PackBackendRoute, PackTurnObserver, PackTurnHooks, } from '@steerable/pack-sdk';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface ApprovalCategoryLabelInput {
|
|
2
|
+
locale: string;
|
|
3
|
+
category: string;
|
|
4
|
+
toolName: string;
|
|
5
|
+
declaredLabel?: Readonly<Record<string, string>>;
|
|
6
|
+
translate: (message: string) => string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Display name for an approval category.
|
|
10
|
+
* Pack declarations win. Built-in categories and shell tools use the shell dictionary.
|
|
11
|
+
*
|
|
12
|
+
* @param input Locale, category, and the tool's declared labels.
|
|
13
|
+
* @returns A label for the card, or undefined when the category is just the tool name.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveApprovalCategoryLabel(input: ApprovalCategoryLabelInput): string | undefined;
|
|
16
|
+
/** English source string for the approval card. The web view translates it. */
|
|
17
|
+
export declare function hostApprovalCategoryLabel(input: {
|
|
18
|
+
category: string;
|
|
19
|
+
toolName: string;
|
|
20
|
+
declaredLabel?: Readonly<Record<string, string>>;
|
|
21
|
+
}): string | undefined;
|
|
22
|
+
export type CwdFence = 'project' | 'workspace';
|
|
23
|
+
/**
|
|
24
|
+
* Which approval sentence to show when `local_exec_shell` names a cwd
|
|
25
|
+
* outside the writable root. A bound project uses its folder and source
|
|
26
|
+
* folders. A chat without a project uses the conversation workspace.
|
|
27
|
+
*
|
|
28
|
+
* @param input Tool name, cwd, and the chat's project or workspace root.
|
|
29
|
+
* @returns `false` when the card should not mention an outside directory.
|
|
30
|
+
*/
|
|
31
|
+
export declare function approvalCwdFence(input: {
|
|
32
|
+
toolName: string;
|
|
33
|
+
cwd: unknown;
|
|
34
|
+
projectRoot: string | null;
|
|
35
|
+
workspaceRoot: string | null;
|
|
36
|
+
sourceFolders: readonly string[];
|
|
37
|
+
home?: string;
|
|
38
|
+
}): CwdFence | false;
|
|
39
|
+
/**
|
|
40
|
+
* True when `local_exec_shell` names a cwd outside the given root and its source folders.
|
|
41
|
+
*
|
|
42
|
+
* The root is the project folder, or the conversation workspace when the chat
|
|
43
|
+
* has no project. `home` defaults to the process home directory, the same
|
|
44
|
+
* directory the shell fence uses when it expands `~`.
|
|
45
|
+
*
|
|
46
|
+
* @param input Tool name, cwd argument, and the chat's writable folders.
|
|
47
|
+
* @returns Whether that cwd is outside those folders.
|
|
48
|
+
*/
|
|
49
|
+
export declare function shellCwdOutsideProject(input: {
|
|
50
|
+
toolName: string;
|
|
51
|
+
cwd: unknown;
|
|
52
|
+
projectRoot: string | null;
|
|
53
|
+
sourceFolders: readonly string[];
|
|
54
|
+
home?: string;
|
|
55
|
+
}): boolean;
|