@steerable/agent-shell 0.6.51 → 0.6.52
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/brand.d.ts +5 -0
- package/dist/goal-store.d.ts +43 -0
- package/dist/goal-store.js +155 -0
- package/dist/host/runtime.js +13 -3
- package/dist/host-tools.d.ts +1 -1
- package/dist/host-tools.js +6 -0
- package/dist/local-backend/router.js +17 -12
- package/dist/local-backend/subagent-profiles.js +4 -0
- package/dist/local-backend/task-service.d.ts +6 -0
- package/dist/local-backend/task-service.js +47 -2
- package/dist/local-executor.d.ts +14 -2
- package/dist/local-executor.js +53 -18
- package/dist/project-home.d.ts +1 -1
- package/dist/project-home.js +1 -1
- package/dist/project-registry.d.ts +4 -4
- package/dist/project-registry.js +2 -2
- package/dist/sidecar/boot.d.ts +5 -2
- package/dist/sidecar/boot.js +1 -0
- package/dist/sidecar/reverse-approval.d.ts +2 -0
- package/dist/sidecar/reverse-approval.js +2 -0
- package/dist/sidecar/reverse-tools.d.ts +6 -2
- package/dist/sidecar/reverse-tools.js +9 -1
- package/dist/sidecar/types.d.ts +10 -1
- package/dist/storage/write-lease-error.js +4 -1
- package/dist/tool-presentation.d.ts +23 -0
- package/dist/tool-presentation.js +266 -0
- package/dist/tool-router.d.ts +26 -8
- package/dist/tool-router.js +254 -18
- package/dist/workspace-search.d.ts +35 -0
- package/dist/workspace-search.js +186 -0
- package/package.json +4 -4
package/dist/tool-router.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { buildProjectRootViolation, isPathWithinRoot, } from './local-executor.js';
|
|
3
|
+
import { buildProjectRootViolation, expandFencePath, isPathWithinRoot, writableFenceRoots, writableFenceViolation, } from './local-executor.js';
|
|
4
4
|
import { mcpExecutor } from './mcp-executor.js';
|
|
5
5
|
import { rankTools, resolveMaxResults, TOOL_SEARCH_DEFAULT_MAX_RESULTS, } from './tool-search-rank.js';
|
|
6
6
|
import { maybeAutoInstallWrittenSkill } from './local-backend/skill-install.js';
|
|
@@ -9,6 +9,8 @@ import { processViewImage } from './image-attachment.js';
|
|
|
9
9
|
import { PRESENT_FILES_SCHEMA, PRESENT_FILES_TOOL_NAME, executePresentFiles, } from './present-files.js';
|
|
10
10
|
import { isHostToolCapabilityEnabled } from './host-tools.js';
|
|
11
11
|
import { getResolvedHostTools } from './host-tools-runtime.js';
|
|
12
|
+
import { GoalStore, GOAL_ACTIONS } from './goal-store.js';
|
|
13
|
+
import { searchGlob, searchGrep } from './workspace-search.js';
|
|
12
14
|
/** 已注册 MCP 服务的动态工具名前缀:`mcp__<serverKey>__<toolName>`。 */
|
|
13
15
|
export const MCP_DYNAMIC_TOOL_PREFIX = 'mcp__';
|
|
14
16
|
/**
|
|
@@ -97,6 +99,16 @@ export class ToolRouter {
|
|
|
97
99
|
this.taskService = services?.taskService ?? null;
|
|
98
100
|
this.worktreeService = services?.worktreeService ?? null;
|
|
99
101
|
}
|
|
102
|
+
/** 会话目标的持久化。缺省写到用户数据目录;测试注入临时文件。 */
|
|
103
|
+
goalStore = null;
|
|
104
|
+
setGoalStore(store) {
|
|
105
|
+
this.goalStore = store;
|
|
106
|
+
}
|
|
107
|
+
goals() {
|
|
108
|
+
if (!this.goalStore)
|
|
109
|
+
this.goalStore = GoalStore.default();
|
|
110
|
+
return this.goalStore;
|
|
111
|
+
}
|
|
100
112
|
setWebTools(delegate, names = []) {
|
|
101
113
|
this.webToolDelegate = delegate;
|
|
102
114
|
this.webToolNames = new Set(names);
|
|
@@ -415,6 +427,7 @@ export class ToolRouter {
|
|
|
415
427
|
additionalProperties: false,
|
|
416
428
|
},
|
|
417
429
|
},
|
|
430
|
+
...this.listParityToolSchemas(),
|
|
418
431
|
// ─── 4.6a/4.6b 跨 turn 后台任务 + git worktree 隔离 ─────────────
|
|
419
432
|
// 服务未接线(CLI/test)时一个 schema 都不出——模型看不到不可用的工具。
|
|
420
433
|
...this.listTaskAndWorktreeSchemas(),
|
|
@@ -482,6 +495,105 @@ export class ToolRouter {
|
|
|
482
495
|
},
|
|
483
496
|
];
|
|
484
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* 内容搜索、PowerShell、会话目标。后台任务的 job_* 跟 task 服务一起出场。
|
|
500
|
+
*/
|
|
501
|
+
listParityToolSchemas() {
|
|
502
|
+
return [
|
|
503
|
+
{
|
|
504
|
+
name: 'grep',
|
|
505
|
+
description: 'Search file contents under the project (or an optional subdirectory). '
|
|
506
|
+
+ 'pattern is a JavaScript regular expression. Optional glob limits which relative paths are scanned. '
|
|
507
|
+
+ 'Skips dependency and VCS directories. Returns {path, line, text} hits. Prefer this over shell rg/grep.',
|
|
508
|
+
mode: 'read',
|
|
509
|
+
inputSchema: {
|
|
510
|
+
type: 'object',
|
|
511
|
+
properties: {
|
|
512
|
+
pattern: { type: 'string', description: 'JavaScript regular expression matched per line.' },
|
|
513
|
+
path: { type: 'string', description: 'Directory to search. Defaults to the project root.' },
|
|
514
|
+
glob: { type: 'string', description: 'Optional fnmatch, e.g. "*.ts" or "src/**/*.py".' },
|
|
515
|
+
},
|
|
516
|
+
required: ['pattern'],
|
|
517
|
+
additionalProperties: false,
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
name: 'glob',
|
|
522
|
+
description: 'List project files whose relative path matches a glob. A pattern without "/" matches any directory. '
|
|
523
|
+
+ 'Prefer this over shell find or ls.',
|
|
524
|
+
mode: 'read',
|
|
525
|
+
inputSchema: {
|
|
526
|
+
type: 'object',
|
|
527
|
+
properties: {
|
|
528
|
+
pattern: { type: 'string', description: 'Glob, e.g. "*.ts" or "src/**/*.py".' },
|
|
529
|
+
path: { type: 'string', description: 'Directory to search. Defaults to the project root.' },
|
|
530
|
+
},
|
|
531
|
+
required: ['pattern'],
|
|
532
|
+
additionalProperties: false,
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
name: 'pwsh',
|
|
537
|
+
description: 'Run a PowerShell command. On Windows this is powershell.exe; elsewhere it runs pwsh '
|
|
538
|
+
+ '(PowerShell 7) and fails clearly when that binary is not installed. '
|
|
539
|
+
+ 'Use local_exec_shell for the platform default shell.',
|
|
540
|
+
mode: 'destructive',
|
|
541
|
+
inputSchema: {
|
|
542
|
+
type: 'object',
|
|
543
|
+
properties: {
|
|
544
|
+
command: { type: 'string' },
|
|
545
|
+
cwd: { type: 'string' },
|
|
546
|
+
timeout: {
|
|
547
|
+
type: 'number',
|
|
548
|
+
description: 'Timeout in milliseconds. A small number like 30 is treated as seconds.',
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
required: ['command'],
|
|
552
|
+
additionalProperties: false,
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
name: 'get_goal',
|
|
557
|
+
description: 'Read this chat\'s persisted goal, including the id and revision that update_goal requires. '
|
|
558
|
+
+ 'goal is null when none exists.',
|
|
559
|
+
mode: 'read',
|
|
560
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
name: 'create_goal',
|
|
564
|
+
description: 'Create a persisted goal for this chat when the user\'s request is a long-running objective. '
|
|
565
|
+
+ 'Not for single-turn work. Fails if an unfinished goal already exists — update that one instead.',
|
|
566
|
+
mode: 'safe_write',
|
|
567
|
+
inputSchema: {
|
|
568
|
+
type: 'object',
|
|
569
|
+
properties: {
|
|
570
|
+
objective: { type: 'string', description: 'What done looks like, as a result rather than an activity.' },
|
|
571
|
+
},
|
|
572
|
+
required: ['objective'],
|
|
573
|
+
additionalProperties: false,
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
name: 'update_goal',
|
|
578
|
+
description: 'Change the current goal. Pass the id and revision from get_goal. '
|
|
579
|
+
+ 'action is edit (needs objective), pause, resume, complete, or blocked (needs reason). '
|
|
580
|
+
+ 'A stale revision is rejected; re-read and retry.',
|
|
581
|
+
mode: 'safe_write',
|
|
582
|
+
inputSchema: {
|
|
583
|
+
type: 'object',
|
|
584
|
+
properties: {
|
|
585
|
+
id: { type: 'string' },
|
|
586
|
+
revision: { type: 'integer' },
|
|
587
|
+
action: { type: 'string', enum: [...GOAL_ACTIONS] },
|
|
588
|
+
objective: { type: 'string', description: 'Required for action=edit.' },
|
|
589
|
+
reason: { type: 'string', description: 'Required for action=blocked.' },
|
|
590
|
+
},
|
|
591
|
+
required: ['id', 'revision', 'action'],
|
|
592
|
+
additionalProperties: false,
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
];
|
|
596
|
+
}
|
|
485
597
|
/**
|
|
486
598
|
* 4.6a/4.6b 任务与 worktree 工具的 schema。task_run 立即返回
|
|
487
599
|
* (任务在后台独立流里跨 turn 跑完);worktree_* 操作绑定项目的
|
|
@@ -594,6 +706,46 @@ export class ToolRouter {
|
|
|
594
706
|
additionalProperties: false,
|
|
595
707
|
},
|
|
596
708
|
},
|
|
709
|
+
{
|
|
710
|
+
name: 'job_list',
|
|
711
|
+
description: 'List this chat\'s background jobs (the same records task_run creates). '
|
|
712
|
+
+ 'Each row includes job_id (the taskId), status, and the task text.',
|
|
713
|
+
mode: 'read',
|
|
714
|
+
inputSchema: {
|
|
715
|
+
type: 'object',
|
|
716
|
+
properties: {},
|
|
717
|
+
additionalProperties: false,
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
name: 'job_output',
|
|
722
|
+
description: 'Read the final answer of a background job. job_id is the taskId from task_run or job_list. '
|
|
723
|
+
+ 'Fails with needsFollowup while the job is still running.',
|
|
724
|
+
mode: 'read',
|
|
725
|
+
inputSchema: {
|
|
726
|
+
type: 'object',
|
|
727
|
+
properties: {
|
|
728
|
+
job_id: { type: 'string', description: 'taskId of the background job.' },
|
|
729
|
+
},
|
|
730
|
+
required: ['job_id'],
|
|
731
|
+
additionalProperties: false,
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
name: 'job_kill',
|
|
736
|
+
description: 'Stop a running or blocked background job. A finished job cannot be killed. '
|
|
737
|
+
+ 'job_id is the taskId from task_run or job_list.',
|
|
738
|
+
mode: 'destructive',
|
|
739
|
+
inputSchema: {
|
|
740
|
+
type: 'object',
|
|
741
|
+
properties: {
|
|
742
|
+
job_id: { type: 'string', description: 'taskId of the background job.' },
|
|
743
|
+
reason: { type: 'string', description: 'Optional note stored on the job.' },
|
|
744
|
+
},
|
|
745
|
+
required: ['job_id'],
|
|
746
|
+
additionalProperties: false,
|
|
747
|
+
},
|
|
748
|
+
},
|
|
597
749
|
{
|
|
598
750
|
name: 'worktree_remove',
|
|
599
751
|
description: 'Remove a managed worktree and delete its branch — the DISCARD path, any unmerged changes are lost. '
|
|
@@ -738,7 +890,7 @@ export class ToolRouter {
|
|
|
738
890
|
command: String(args.command || ''),
|
|
739
891
|
cwd: typeof args.cwd === 'string' ? args.cwd : undefined,
|
|
740
892
|
timeout: typeof args.timeout === 'number' ? args.timeout : undefined,
|
|
741
|
-
}, projectRoot);
|
|
893
|
+
}, projectRoot, context?.additionalWriteRoots ?? null);
|
|
742
894
|
case 'local_read_file':
|
|
743
895
|
return await this.localExecutor.readLocalFile({
|
|
744
896
|
path: String(args.path || ''),
|
|
@@ -776,7 +928,7 @@ export class ToolRouter {
|
|
|
776
928
|
content: String(args.content || ''),
|
|
777
929
|
createDirs: Boolean(args.createDirs),
|
|
778
930
|
expectedVersion: typeof args.expectedVersion === 'string' ? args.expectedVersion : undefined,
|
|
779
|
-
}, projectRoot);
|
|
931
|
+
}, projectRoot, context?.additionalWriteRoots ?? null);
|
|
780
932
|
this.autoInstallSkillOnSuccess(written, args.path);
|
|
781
933
|
return written;
|
|
782
934
|
}
|
|
@@ -793,7 +945,7 @@ export class ToolRouter {
|
|
|
793
945
|
path: String(args.path || ''),
|
|
794
946
|
edits,
|
|
795
947
|
expectedVersion: typeof args.expectedVersion === 'string' ? args.expectedVersion : undefined,
|
|
796
|
-
}, projectRoot);
|
|
948
|
+
}, projectRoot, context?.additionalWriteRoots ?? null);
|
|
797
949
|
this.autoInstallSkillOnSuccess(edited, args.path);
|
|
798
950
|
return edited;
|
|
799
951
|
}
|
|
@@ -805,7 +957,7 @@ export class ToolRouter {
|
|
|
805
957
|
});
|
|
806
958
|
case 'local_run_snippet': {
|
|
807
959
|
// 与 local_exec_shell 同款项目沙箱:cwd 默认项目根,显式越界拒绝。
|
|
808
|
-
const sandboxed = this.applyProjectCwdSandbox({ cwd: typeof args.cwd === 'string' ? args.cwd : undefined }, projectRoot);
|
|
960
|
+
const sandboxed = this.applyProjectCwdSandbox({ cwd: typeof args.cwd === 'string' ? args.cwd : undefined }, projectRoot, context?.additionalWriteRoots ?? null);
|
|
809
961
|
if ('error' in sandboxed)
|
|
810
962
|
return sandboxed.error;
|
|
811
963
|
return await this.localExecutor.runCode({
|
|
@@ -859,6 +1011,47 @@ export class ToolRouter {
|
|
|
859
1011
|
await this.refreshPluginTools();
|
|
860
1012
|
return result;
|
|
861
1013
|
}
|
|
1014
|
+
case 'grep':
|
|
1015
|
+
case 'glob': {
|
|
1016
|
+
const requested = typeof args.path === 'string' ? args.path.trim() : '';
|
|
1017
|
+
const start = requested
|
|
1018
|
+
? this.resolveReadablePath(requested, projectRoot, context?.additionalReadRoots ?? null)
|
|
1019
|
+
: { path: projectRoot ? expandFencePath(projectRoot) : process.cwd() };
|
|
1020
|
+
if ('error' in start) {
|
|
1021
|
+
return { success: false, error: start.error, needsFollowup: true };
|
|
1022
|
+
}
|
|
1023
|
+
if (call.name === 'grep') {
|
|
1024
|
+
return searchGrep({
|
|
1025
|
+
root: start.path,
|
|
1026
|
+
pattern: String(args.pattern || ''),
|
|
1027
|
+
glob: typeof args.glob === 'string' ? args.glob : undefined,
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
return searchGlob({ root: start.path, pattern: String(args.pattern || '') });
|
|
1031
|
+
}
|
|
1032
|
+
case 'pwsh': {
|
|
1033
|
+
const script = String(args.command || '');
|
|
1034
|
+
if (!script.trim()) {
|
|
1035
|
+
return { success: false, error: 'command is required', needsFollowup: true };
|
|
1036
|
+
}
|
|
1037
|
+
const command = process.platform === 'win32'
|
|
1038
|
+
? script
|
|
1039
|
+
: `pwsh -NoProfile -NonInteractive -Command ${JSON.stringify(script)}`;
|
|
1040
|
+
return await this.executeShell({
|
|
1041
|
+
command,
|
|
1042
|
+
cwd: typeof args.cwd === 'string' ? args.cwd : undefined,
|
|
1043
|
+
timeout: typeof args.timeout === 'number' ? args.timeout : undefined,
|
|
1044
|
+
shell: process.platform === 'win32' ? 'powershell' : undefined,
|
|
1045
|
+
}, projectRoot, context?.additionalWriteRoots ?? null);
|
|
1046
|
+
}
|
|
1047
|
+
case 'get_goal':
|
|
1048
|
+
case 'create_goal':
|
|
1049
|
+
case 'update_goal':
|
|
1050
|
+
return this.executeGoalTool(call.name, args, context);
|
|
1051
|
+
case 'job_list':
|
|
1052
|
+
case 'job_output':
|
|
1053
|
+
case 'job_kill':
|
|
1054
|
+
return this.executeJobTool(call.name, args, context);
|
|
862
1055
|
case 'task_run': {
|
|
863
1056
|
const chatId = this.requireTaskContext(context);
|
|
864
1057
|
const dependsOn = Array.isArray(args.dependsOn)
|
|
@@ -927,6 +1120,46 @@ export class ToolRouter {
|
|
|
927
1120
|
* 项目围栏按 chat 解析)。schema 出场 ⇒ 服务已接线,这里只缺 chatId 时
|
|
928
1121
|
* 抛错(reverse-tools 总是透传;缺了就是接线 bug,响亮失败)。
|
|
929
1122
|
*/
|
|
1123
|
+
async executeGoalTool(name, args, context) {
|
|
1124
|
+
const chatId = context?.chatId;
|
|
1125
|
+
if (!chatId) {
|
|
1126
|
+
return { success: false, error: '目标工具需要 chatId 调用上下文', needsFollowup: true };
|
|
1127
|
+
}
|
|
1128
|
+
const store = this.goals();
|
|
1129
|
+
if (name === 'get_goal')
|
|
1130
|
+
return store.get(chatId);
|
|
1131
|
+
if (name === 'create_goal')
|
|
1132
|
+
return store.create(chatId, String(args.objective || ''));
|
|
1133
|
+
return store.update({
|
|
1134
|
+
chatId,
|
|
1135
|
+
id: String(args.id || ''),
|
|
1136
|
+
revision: typeof args.revision === 'number' ? args.revision : Number.NaN,
|
|
1137
|
+
action: String(args.action || ''),
|
|
1138
|
+
objective: typeof args.objective === 'string' ? args.objective : undefined,
|
|
1139
|
+
reason: typeof args.reason === 'string' ? args.reason : undefined,
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
async executeJobTool(name, args, context) {
|
|
1143
|
+
const chatId = this.requireTaskContext(context);
|
|
1144
|
+
const jobId = String(args.job_id || '');
|
|
1145
|
+
if (name === 'job_list') {
|
|
1146
|
+
const listed = await this.taskService.status(chatId);
|
|
1147
|
+
const tasks = Array.isArray(listed.tasks) ? listed.tasks : [];
|
|
1148
|
+
return {
|
|
1149
|
+
...listed,
|
|
1150
|
+
jobs: tasks.map((task) => {
|
|
1151
|
+
const row = task;
|
|
1152
|
+
return { ...row, job_id: row.taskId };
|
|
1153
|
+
}),
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
if (!jobId)
|
|
1157
|
+
return { success: false, error: 'job_id 不能为空', needsFollowup: true };
|
|
1158
|
+
if (name === 'job_output')
|
|
1159
|
+
return this.taskService.result(chatId, jobId);
|
|
1160
|
+
const reason = typeof args.reason === 'string' && args.reason.trim() ? args.reason.trim() : undefined;
|
|
1161
|
+
return this.taskService.kill(chatId, jobId, reason);
|
|
1162
|
+
}
|
|
930
1163
|
requireTaskContext(context) {
|
|
931
1164
|
if (!this.taskService || !this.worktreeService) {
|
|
932
1165
|
throw new Error('任务/worktree 服务未接线(main.ts setTaskServices)');
|
|
@@ -955,41 +1188,44 @@ export class ToolRouter {
|
|
|
955
1188
|
}
|
|
956
1189
|
}
|
|
957
1190
|
/**
|
|
958
|
-
* 项目模式硬沙箱(cwd
|
|
959
|
-
* 模型显式给的 cwd
|
|
960
|
-
*
|
|
1191
|
+
* 项目模式硬沙箱(cwd 维度):有可写根时,cwd 默认收到项目家目录;
|
|
1192
|
+
* 模型显式给的 cwd 必须落在家目录或额外可写根(源文件夹)内,含各自子目录。
|
|
1193
|
+
* 相对 cwd 按家目录解析(而不是进程 cwd)。
|
|
961
1194
|
*
|
|
962
1195
|
* 注意边界:shell 命令文本里内嵌的绝对路径无法可靠解析,围栏只覆盖
|
|
963
|
-
* cwd
|
|
1196
|
+
* cwd 与文件读写工具。命令实际写盘还受 exec 沙箱的 writableRoots 约束,
|
|
1197
|
+
* 那些根同样包含子目录。
|
|
964
1198
|
*
|
|
965
1199
|
* local_exec_shell 与 local_run_snippet 共用本 helper。
|
|
966
1200
|
*/
|
|
967
|
-
applyProjectCwdSandbox(request, projectRoot) {
|
|
968
|
-
|
|
1201
|
+
applyProjectCwdSandbox(request, projectRoot, additionalWriteRoots) {
|
|
1202
|
+
const roots = writableFenceRoots(projectRoot, additionalWriteRoots);
|
|
1203
|
+
if (roots.length === 0)
|
|
969
1204
|
return { request };
|
|
970
|
-
const
|
|
1205
|
+
const home = projectRoot ? expandFencePath(projectRoot) : roots[0];
|
|
971
1206
|
if (request.cwd) {
|
|
972
1207
|
const expanded = request.cwd.startsWith('~')
|
|
973
1208
|
? path.join(os.homedir(), request.cwd.slice(1))
|
|
974
1209
|
: request.cwd;
|
|
975
1210
|
const resolvedCwd = path.isAbsolute(expanded)
|
|
976
1211
|
? path.resolve(expanded)
|
|
977
|
-
: path.resolve(
|
|
978
|
-
|
|
1212
|
+
: path.resolve(home, expanded);
|
|
1213
|
+
const violation = writableFenceViolation(resolvedCwd, roots);
|
|
1214
|
+
if (violation) {
|
|
979
1215
|
return {
|
|
980
1216
|
error: {
|
|
981
1217
|
success: false,
|
|
982
|
-
error:
|
|
1218
|
+
error: violation,
|
|
983
1219
|
platform: process.platform,
|
|
984
1220
|
},
|
|
985
1221
|
};
|
|
986
1222
|
}
|
|
987
1223
|
return { request: { ...request, cwd: resolvedCwd } };
|
|
988
1224
|
}
|
|
989
|
-
return { request: { ...request, cwd:
|
|
1225
|
+
return { request: { ...request, cwd: home } };
|
|
990
1226
|
}
|
|
991
|
-
async executeShell(request, projectRoot) {
|
|
992
|
-
const sandboxed = this.applyProjectCwdSandbox(request, projectRoot);
|
|
1227
|
+
async executeShell(request, projectRoot, additionalWriteRoots) {
|
|
1228
|
+
const sandboxed = this.applyProjectCwdSandbox(request, projectRoot, additionalWriteRoots);
|
|
993
1229
|
if ('error' in sandboxed)
|
|
994
1230
|
return sandboxed.error;
|
|
995
1231
|
if (this.shellExecutor) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 宿主侧 grep / glob。
|
|
3
|
+
*
|
|
4
|
+
* 调用方先把起点收进项目围栏。这里只在该目录内遍历,跳过符号链接,
|
|
5
|
+
* 并丢掉依赖目录和超大文件。命中数有上限。
|
|
6
|
+
*/
|
|
7
|
+
export interface SearchHit {
|
|
8
|
+
path: string;
|
|
9
|
+
line: number;
|
|
10
|
+
text: string;
|
|
11
|
+
}
|
|
12
|
+
export interface GrepResult {
|
|
13
|
+
success: boolean;
|
|
14
|
+
matches?: SearchHit[];
|
|
15
|
+
truncated?: boolean;
|
|
16
|
+
filesScanned?: number;
|
|
17
|
+
error?: string;
|
|
18
|
+
needsFollowup?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface GlobResult {
|
|
21
|
+
success: boolean;
|
|
22
|
+
files?: string[];
|
|
23
|
+
truncated?: boolean;
|
|
24
|
+
error?: string;
|
|
25
|
+
needsFollowup?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare function searchGrep(input: {
|
|
28
|
+
root: string;
|
|
29
|
+
pattern: string;
|
|
30
|
+
glob?: string;
|
|
31
|
+
}): Promise<GrepResult>;
|
|
32
|
+
export declare function searchGlob(input: {
|
|
33
|
+
root: string;
|
|
34
|
+
pattern: string;
|
|
35
|
+
}): Promise<GlobResult>;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 宿主侧 grep / glob。
|
|
3
|
+
*
|
|
4
|
+
* 调用方先把起点收进项目围栏。这里只在该目录内遍历,跳过符号链接,
|
|
5
|
+
* 并丢掉依赖目录和超大文件。命中数有上限。
|
|
6
|
+
*/
|
|
7
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
const SKIP_DIRS = new Set([
|
|
10
|
+
'node_modules',
|
|
11
|
+
'.git',
|
|
12
|
+
'dist',
|
|
13
|
+
'build',
|
|
14
|
+
'target',
|
|
15
|
+
'.steerable',
|
|
16
|
+
'__pycache__',
|
|
17
|
+
'.venv',
|
|
18
|
+
'venv',
|
|
19
|
+
]);
|
|
20
|
+
const MAX_FILES = 4_000;
|
|
21
|
+
const MAX_MATCHES = 80;
|
|
22
|
+
const MAX_FILE_BYTES = 1_000_000;
|
|
23
|
+
const MAX_DEPTH = 20;
|
|
24
|
+
function globToRegExp(pattern) {
|
|
25
|
+
const normalized = pattern.replaceAll('\\', '/');
|
|
26
|
+
const withPrefix = normalized.includes('/') ? normalized : `**/${normalized}`;
|
|
27
|
+
let out = '';
|
|
28
|
+
for (let i = 0; i < withPrefix.length; i += 1) {
|
|
29
|
+
const char = withPrefix[i];
|
|
30
|
+
if (char === '*' && withPrefix[i + 1] === '*') {
|
|
31
|
+
if (withPrefix[i + 2] === '/') {
|
|
32
|
+
out += '(?:.*/)?';
|
|
33
|
+
i += 2;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
out += '.*';
|
|
37
|
+
i += 1;
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (char === '*') {
|
|
42
|
+
out += '[^/]*';
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (char === '?') {
|
|
46
|
+
out += '[^/]';
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if ('\\^$+?.()|{}[]'.includes(char))
|
|
50
|
+
out += `\\${char}`;
|
|
51
|
+
else
|
|
52
|
+
out += char;
|
|
53
|
+
}
|
|
54
|
+
return new RegExp(`^${out}$`);
|
|
55
|
+
}
|
|
56
|
+
async function walk(root, visit) {
|
|
57
|
+
let files = 0;
|
|
58
|
+
let truncated = false;
|
|
59
|
+
const queue = [{ dir: root, depth: 0 }];
|
|
60
|
+
while (queue.length > 0) {
|
|
61
|
+
const next = queue.pop();
|
|
62
|
+
if (!next)
|
|
63
|
+
break;
|
|
64
|
+
let entries;
|
|
65
|
+
try {
|
|
66
|
+
entries = await readdir(next.dir, { withFileTypes: true });
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (entry.isSymbolicLink())
|
|
73
|
+
continue;
|
|
74
|
+
const absolute = path.join(next.dir, entry.name);
|
|
75
|
+
if (entry.isDirectory()) {
|
|
76
|
+
if (SKIP_DIRS.has(entry.name) || next.depth >= MAX_DEPTH)
|
|
77
|
+
continue;
|
|
78
|
+
queue.push({ dir: absolute, depth: next.depth + 1 });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!entry.isFile())
|
|
82
|
+
continue;
|
|
83
|
+
files += 1;
|
|
84
|
+
if (files > MAX_FILES) {
|
|
85
|
+
truncated = true;
|
|
86
|
+
return { truncated, files };
|
|
87
|
+
}
|
|
88
|
+
const relative = path.relative(root, absolute).split(path.sep).join('/');
|
|
89
|
+
const stop = await visit(absolute, relative);
|
|
90
|
+
if (stop === true) {
|
|
91
|
+
truncated = true;
|
|
92
|
+
return { truncated, files };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { truncated, files };
|
|
97
|
+
}
|
|
98
|
+
export async function searchGrep(input) {
|
|
99
|
+
const pattern = input.pattern.trim();
|
|
100
|
+
if (!pattern)
|
|
101
|
+
return { success: false, error: 'pattern 不能为空', needsFollowup: true };
|
|
102
|
+
let matcher;
|
|
103
|
+
try {
|
|
104
|
+
matcher = new RegExp(pattern);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
108
|
+
return { success: false, error: `无效的正则:${message}`, needsFollowup: true };
|
|
109
|
+
}
|
|
110
|
+
let fileFilter = null;
|
|
111
|
+
if (input.glob && input.glob.trim()) {
|
|
112
|
+
try {
|
|
113
|
+
fileFilter = globToRegExp(input.glob.trim());
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
117
|
+
return { success: false, error: `无效的 glob:${message}`, needsFollowup: true };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const matches = [];
|
|
121
|
+
const walked = await walk(input.root, async (absolute, relative) => {
|
|
122
|
+
if (fileFilter && !fileFilter.test(relative))
|
|
123
|
+
return;
|
|
124
|
+
let info;
|
|
125
|
+
try {
|
|
126
|
+
info = await stat(absolute);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (!info.isFile() || info.size > MAX_FILE_BYTES)
|
|
132
|
+
return;
|
|
133
|
+
let text;
|
|
134
|
+
try {
|
|
135
|
+
text = await readFile(absolute, 'utf8');
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (text.includes('\0'))
|
|
141
|
+
return;
|
|
142
|
+
const lines = text.split(/\r?\n/);
|
|
143
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
144
|
+
if (!matcher.test(lines[i]))
|
|
145
|
+
continue;
|
|
146
|
+
matches.push({ path: relative, line: i + 1, text: lines[i].slice(0, 400) });
|
|
147
|
+
if (matches.length >= MAX_MATCHES)
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
});
|
|
152
|
+
return {
|
|
153
|
+
success: true,
|
|
154
|
+
matches,
|
|
155
|
+
truncated: walked.truncated || matches.length >= MAX_MATCHES,
|
|
156
|
+
filesScanned: walked.files,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
export async function searchGlob(input) {
|
|
160
|
+
const pattern = input.pattern.trim();
|
|
161
|
+
if (!pattern)
|
|
162
|
+
return { success: false, error: 'pattern 不能为空', needsFollowup: true };
|
|
163
|
+
let matcher;
|
|
164
|
+
try {
|
|
165
|
+
matcher = globToRegExp(pattern);
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
169
|
+
return { success: false, error: `无效的 glob:${message}`, needsFollowup: true };
|
|
170
|
+
}
|
|
171
|
+
const files = [];
|
|
172
|
+
const walked = await walk(input.root, async (_absolute, relative) => {
|
|
173
|
+
if (!matcher.test(relative))
|
|
174
|
+
return;
|
|
175
|
+
files.push(relative);
|
|
176
|
+
if (files.length >= MAX_MATCHES)
|
|
177
|
+
return true;
|
|
178
|
+
return undefined;
|
|
179
|
+
});
|
|
180
|
+
files.sort();
|
|
181
|
+
return {
|
|
182
|
+
success: true,
|
|
183
|
+
files,
|
|
184
|
+
truncated: walked.truncated || files.length >= MAX_MATCHES,
|
|
185
|
+
};
|
|
186
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-shell",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.52",
|
|
4
4
|
"description": "Steerable framework — product-neutral Node HostRuntime and headless HTTP server shared by Tauri desktop and browser deployments; scenario packs extend it through @steerable/pack-sdk.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://steerableframework.com/",
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
"he": "^1.2.0",
|
|
56
56
|
"node-pty": "^1.1.0",
|
|
57
57
|
"semver": "^7.8.0",
|
|
58
|
-
"@steerable/agent-harness": "0.6.
|
|
59
|
-
"@steerable/agent-protocol": "0.6.
|
|
60
|
-
"@steerable/pack-sdk": "0.6.
|
|
58
|
+
"@steerable/agent-harness": "0.6.52",
|
|
59
|
+
"@steerable/agent-protocol": "0.6.52",
|
|
60
|
+
"@steerable/pack-sdk": "0.6.52"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/better-sqlite3": "^7.6.13",
|