@steerable/agent-shell 0.6.17 → 0.6.19

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.
@@ -898,7 +898,19 @@ export class LocalBackendRouter {
898
898
  }
899
899
  }
900
900
  if (method === 'DELETE' && pathname.startsWith('/api/v2/chat-agents/skills/delete/')) {
901
- const skillName = pathname.slice('/api/v2/chat-agents/skills/delete/'.length).trim();
901
+ // URL.pathname 保留百分号编码——目录名/frontmatter 名含空格等字符时
902
+ // 客户端发来的是 fancy%20skill,不 decode 永远匹配不上 fancy skill
903
+ // (projects/mcp 路由段都 decode,这里对齐)。非法编码按 400 处理。
904
+ let skillName;
905
+ try {
906
+ skillName = decodeURIComponent(pathname.slice('/api/v2/chat-agents/skills/delete/'.length)).trim();
907
+ }
908
+ catch {
909
+ return {
910
+ status: 400,
911
+ data: { error: 'skillName is not valid percent-encoding' },
912
+ };
913
+ }
902
914
  if (!skillName) {
903
915
  return {
904
916
  status: 400,
@@ -1743,6 +1755,14 @@ export class LocalBackendRouter {
1743
1755
  // user+assistant pair onto history while the original (possibly bad)
1744
1756
  // assistant reply stayed in place — the model saw both and had no real
1745
1757
  // reason to answer differently.
1758
+ //
1759
+ // sidecar 门必须先于任何写操作:rerun 回合只能跑在 sidecar 上
1760
+ // (handleCoreLoopTurn 无 sidecar 直接 503)。若先截断再 503,旧回复
1761
+ // 已删、新回复不会产生、record 也不存在——非破坏性承诺破窗。
1762
+ if (!getSidecarSupervisor()) {
1763
+ emit(this.sse('error', { message: 'coreloop enabled but sidecar is not running' }));
1764
+ return { status: 503 };
1765
+ }
1746
1766
  const targetMessageId = regenerateMatch[2];
1747
1767
  // The user turn that prompted the target reply is whatever immediately
1748
1768
  // precedes it — re-derive its text so buildConversationMessages gets
@@ -2897,9 +2917,14 @@ export class LocalBackendRouter {
2897
2917
  onNotice: (kind, notice) => {
2898
2918
  if (kind === 'budget_exhausted') {
2899
2919
  // 预算维度(rounds/tokens)在 notice.budget;notice.kind 是信封
2900
- // 类型(loop 事件 data 里的 budget 键经 sidecar 透传)。
2920
+ // 类型(loop 事件 data 里的 budget 键经 sidecar 透传)。message
2921
+ // 是客户端读取的人类可读原因(chat-transport 的 reason 字段)。
2901
2922
  const budgetKind = typeof notice?.budget === 'string' ? notice.budget : undefined;
2902
- emit(this.sseData({ type: 'budget_exhausted', budget: { kind: budgetKind } }));
2923
+ emit(this.sseData({
2924
+ type: 'budget_exhausted',
2925
+ budget: { kind: budgetKind },
2926
+ message: budgetKind ? `budget_exhausted: ${budgetKind}` : 'budget_exhausted',
2927
+ }));
2903
2928
  }
2904
2929
  },
2905
2930
  onChildEvent: (event) => {
@@ -30,12 +30,22 @@ export interface LocalFileReadRequest {
30
30
  path: string;
31
31
  encoding?: BufferEncoding;
32
32
  maxSize?: number;
33
+ /** 1-based 起始行(CC Read 风格分页)。与 limit 配合做部分视图读取。 */
34
+ offset?: number;
35
+ /** 最多返回的行数(配合 offset 分页);缺省返回到文件尾。 */
36
+ limit?: number;
33
37
  }
34
38
  export interface LocalFileReadResult {
35
39
  success: boolean;
36
40
  content?: string;
37
41
  /** 内容版本令牌(SHA-256)。读后写场景把它回传给 edit/write 做冲突检测。 */
38
42
  version?: string;
43
+ /**
44
+ * 本次返回的只是部分视图(offset/limit 分页或超长裁剪)——模型没看到完整
45
+ * 内容,本会话对该路径的整体覆写将被拒绝(框架 partial_reads / CC
46
+ * isPartialView 对齐);定点修改走 local_edit_file 不受此限。
47
+ */
48
+ partial?: boolean;
39
49
  error?: string;
40
50
  }
41
51
  export interface LocalFileWriteRequest {
@@ -105,6 +115,18 @@ export interface LocalRunCodeResult extends LocalExecResult {
105
115
  /** 本次调用实际安装成功的依赖。 */
106
116
  installedPackages?: string[];
107
117
  }
118
+ /**
119
+ * 部分视图裁剪(框架 `_clip` / CC Read 对齐)。version 永远基于完整内容计算,
120
+ * 部分视图的 CAS 令牌保持有效;返回的 partial 标记驱动整体覆写门。
121
+ *
122
+ * - 给了 offset/limit:按 1-based 行号切片,并在省略处打行数标记(含继续
123
+ * 分页的下一个 offset);
124
+ * - 没给但内容超长:头尾裁剪,中间打省略字符数标记。
125
+ */
126
+ export declare function slicePartialView(content: string, offset?: number, limit?: number): {
127
+ content: string;
128
+ partial: boolean;
129
+ };
108
130
  /** 内容版本令牌:UTF-8 内容的 SHA-256。用于 read-before-write 冲突检测。 */
109
131
  /**
110
132
  * read-before-write 冲突令牌:内容的 SHA-256(UTF-8)十六进制。与框架
@@ -167,6 +189,13 @@ export declare class LocalExecutor {
167
189
  * 由 sidecar 的 `read_state.seed` 反向调用重灌(seedReadState)。
168
190
  */
169
191
  private readonly readFileState;
192
+ /**
193
+ * 部分视图门(框架 partial_reads / CC isPartialView 对齐):本会话只读到
194
+ * 部分内容的分页/裁剪读取把路径登记在这里;writeLocalFile 拒绝整体覆写
195
+ * 未见全文的文件。完整读取或本会话自己的写/编辑成功后销记。会话恢复不
196
+ * 重灌此集合(与框架一致:resume 只重建 version 证据,门退化为 CAS)。
197
+ */
198
+ private readonly partialReadPaths;
170
199
  constructor(maxOutputBytes?: number, applyEditsFn?: ApplyEditsFn);
171
200
  updateSafetyConfig(config: CommandSafetyConfigPayload): void;
172
201
  /**
@@ -61,6 +61,55 @@ const NPM_PACKAGE_RE = /^(@[A-Za-z0-9._-]+\/)?[A-Za-z0-9._-]+(@[A-Za-z0-9._~^<>=
61
61
  const DEFAULT_TIMEOUT_MS = 30_000;
62
62
  const DEFAULT_MAX_OUTPUT_BYTES = 100 * 1024;
63
63
  const DEFAULT_MAX_READ_SIZE = 1024 * 1024;
64
+ /**
65
+ * 单次读取返回给模型的字符上限(框架 workspace_tools `_MAX_OUTPUT` 对齐)。
66
+ * 超出部分头尾裁剪并打标记——保留尾部,日志/编译错误的关键行通常在末尾。
67
+ */
68
+ const MAX_READ_OUTPUT_CHARS = 100_000;
69
+ /**
70
+ * 部分视图裁剪(框架 `_clip` / CC Read 对齐)。version 永远基于完整内容计算,
71
+ * 部分视图的 CAS 令牌保持有效;返回的 partial 标记驱动整体覆写门。
72
+ *
73
+ * - 给了 offset/limit:按 1-based 行号切片,并在省略处打行数标记(含继续
74
+ * 分页的下一个 offset);
75
+ * - 没给但内容超长:头尾裁剪,中间打省略字符数标记。
76
+ */
77
+ export function slicePartialView(content, offset, limit) {
78
+ if (offset !== undefined || limit !== undefined) {
79
+ const start = offset ?? 1;
80
+ if (!Number.isInteger(start) || start < 1) {
81
+ throw new Error(`offset 必须是 >= 1 的整数(1-based 行号),收到 ${offset}`);
82
+ }
83
+ if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) {
84
+ throw new Error(`limit 必须是 >= 1 的整数,收到 ${limit}`);
85
+ }
86
+ const lines = content.split('\n');
87
+ if (start > lines.length) {
88
+ throw new Error(`offset ${start} 超出文件行数(共 ${lines.length} 行)。`);
89
+ }
90
+ const end = limit === undefined ? lines.length : Math.min(lines.length, start - 1 + limit);
91
+ let view = lines.slice(start - 1, end).join('\n');
92
+ const omittedAbove = start - 1;
93
+ const omittedBelow = lines.length - end;
94
+ if (omittedAbove > 0) {
95
+ view = `...[{上文省略 ${omittedAbove} 行}]...\n${view}`;
96
+ }
97
+ if (omittedBelow > 0) {
98
+ view = `${view}\n...[{下文省略 ${omittedBelow} 行;用 offset=${end + 1} 继续}]...`;
99
+ }
100
+ return { content: view, partial: omittedAbove > 0 || omittedBelow > 0 };
101
+ }
102
+ if (content.length <= MAX_READ_OUTPUT_CHARS) {
103
+ return { content, partial: false };
104
+ }
105
+ const head = Math.floor(MAX_READ_OUTPUT_CHARS / 5);
106
+ const tail = MAX_READ_OUTPUT_CHARS - head;
107
+ const omitted = content.length - head - tail;
108
+ return {
109
+ content: `${content.slice(0, head)}\n...[{省略 ${omitted} 字符;用 offset/limit 分段读取}]...\n${content.slice(-tail)}`,
110
+ partial: true,
111
+ };
112
+ }
64
113
  /** 内容版本令牌:UTF-8 内容的 SHA-256。用于 read-before-write 冲突检测。 */
65
114
  /**
66
115
  * read-before-write 冲突令牌:内容的 SHA-256(UTF-8)十六进制。与框架
@@ -175,6 +224,13 @@ export class LocalExecutor {
175
224
  * 由 sidecar 的 `read_state.seed` 反向调用重灌(seedReadState)。
176
225
  */
177
226
  readFileState = new Map();
227
+ /**
228
+ * 部分视图门(框架 partial_reads / CC isPartialView 对齐):本会话只读到
229
+ * 部分内容的分页/裁剪读取把路径登记在这里;writeLocalFile 拒绝整体覆写
230
+ * 未见全文的文件。完整读取或本会话自己的写/编辑成功后销记。会话恢复不
231
+ * 重灌此集合(与框架一致:resume 只重建 version 证据,门退化为 CAS)。
232
+ */
233
+ partialReadPaths = new Set();
178
234
  constructor(maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES, applyEditsFn = applyEdits) {
179
235
  this.maxOutputBytes = maxOutputBytes;
180
236
  this.applyEditsFn = applyEditsFn;
@@ -405,10 +461,19 @@ export class LocalExecutor {
405
461
  return { success: false, error: `File too large (${fileStat.size} bytes), max=${maxSize}` };
406
462
  }
407
463
  const content = await readFile(filePath, { encoding });
464
+ // version 基于完整内容:部分视图的读证据 CAS 仍然有效(框架对齐)。
408
465
  const version = hashContent(content);
409
466
  // P2b: 记录读证据,后续 write/edit 缺省 expectedVersion 时自动 CAS。
410
467
  this.readFileState.set(filePath, version);
411
- return { success: true, content, version };
468
+ const view = slicePartialView(content, request.offset, request.limit);
469
+ // 部分视图登记/销记:整体覆写门(writeLocalFile)据此拒绝未见全文的覆写。
470
+ if (view.partial) {
471
+ this.partialReadPaths.add(filePath);
472
+ }
473
+ else {
474
+ this.partialReadPaths.delete(filePath);
475
+ }
476
+ return { success: true, content: view.content, version, partial: view.partial };
412
477
  }
413
478
  catch (error) {
414
479
  const message = error instanceof Error ? error.message : String(error);
@@ -427,6 +492,17 @@ export class LocalExecutor {
427
492
  return this.serializeFileOp(filePath, async () => {
428
493
  try {
429
494
  const encoding = request.encoding ?? 'utf-8';
495
+ // 部分视图门:本会话对该路径只读到分页/裁剪后的内容,整体覆写会销毁
496
+ // 未见内容(框架 partial_reads / CC isPartialView 对齐)。定点修改走
497
+ // local_edit_file;确需整体重写时先用 offset/limit 分段读完。
498
+ if (this.partialReadPaths.has(filePath)) {
499
+ return {
500
+ success: false,
501
+ error: `拒绝整体覆写:${filePath} 本会话只读到部分内容(分页/裁剪),` +
502
+ '覆写会销毁未见内容。请改用 local_edit_file 做定点修改;' +
503
+ '确需整体重写时先用 offset/limit 分段读完。',
504
+ };
505
+ }
430
506
  // P2b 自动 CAS:调用方没给 expectedVersion 时回落到本会话的读证据
431
507
  // (含 read_state.seed 重灌的)。隐式期望只对仍存在的文件生效——
432
508
  // 读后被删除的文件重新写入是新建,没有可覆盖的他人改动。
@@ -453,6 +529,8 @@ export class LocalExecutor {
453
529
  const version = hashContent(request.content);
454
530
  // 写成功后回写读证据:本会话自己的连续写不互相冲突。
455
531
  this.readFileState.set(filePath, version);
532
+ // 模型亲自写了全文,部分视图门不再适用(框架对齐)。
533
+ this.partialReadPaths.delete(filePath);
456
534
  return { success: true, version };
457
535
  }
458
536
  catch (error) {
@@ -498,6 +576,8 @@ export class LocalExecutor {
498
576
  await this.atomicWrite(filePath, result.content, encoding);
499
577
  const version = hashContent(result.content);
500
578
  this.readFileState.set(filePath, version);
579
+ // 注意:编辑不销记 partialReadPaths——定点编辑不代表模型见过全文,
580
+ // 后续盲写仍会销毁未见内容(框架「Cleared by a full read or a write」对齐)。
501
581
  return {
502
582
  success: true,
503
583
  version,
package/dist/preload.cjs CHANGED
@@ -122,14 +122,14 @@ function buildPreloadApi(packContributions = {}) {
122
122
  },
123
123
  /**
124
124
  * 4.6a 后台任务状态推送:任务到达终态(completed/failed)或 worktree
125
- * 合并/丢弃完成时,主进程广播 `task-updated`。载荷只带 chatId/taskId/
126
- * status——renderer 收到后重新拉任务列表(GET /chats/:id/tasks),
125
+ * 合并/丢弃完成时,主进程广播 `task-updated`。载荷只带 chatId/taskId
126
+ * ——renderer 收到后重新拉任务列表(GET /chats/:id/tasks),
127
127
  * 不把整条任务记录塞进 IPC(避免与 SQLite 双写漂移)。
128
128
  */
129
129
  onTaskUpdated: (callback) => {
130
130
  const handler = (_event, payload) => {
131
- if (typeof payload?.chatId === "string" && typeof payload?.taskId === "string" && typeof payload?.status === "string") {
132
- callback({ chatId: payload.chatId, taskId: payload.taskId, status: payload.status });
131
+ if (typeof payload?.chatId === "string" && typeof payload?.taskId === "string") {
132
+ callback({ chatId: payload.chatId, taskId: payload.taskId });
133
133
  }
134
134
  };
135
135
  import_electron.ipcRenderer.on("task-updated", handler);
package/dist/preload.js CHANGED
@@ -138,16 +138,14 @@ export function buildPreloadApi(packContributions = {}) {
138
138
  },
139
139
  /**
140
140
  * 4.6a 后台任务状态推送:任务到达终态(completed/failed)或 worktree
141
- * 合并/丢弃完成时,主进程广播 `task-updated`。载荷只带 chatId/taskId/
142
- * status——renderer 收到后重新拉任务列表(GET /chats/:id/tasks),
141
+ * 合并/丢弃完成时,主进程广播 `task-updated`。载荷只带 chatId/taskId
142
+ * ——renderer 收到后重新拉任务列表(GET /chats/:id/tasks),
143
143
  * 不把整条任务记录塞进 IPC(避免与 SQLite 双写漂移)。
144
144
  */
145
145
  onTaskUpdated: (callback) => {
146
146
  const handler = (_event, payload) => {
147
- if (typeof payload?.chatId === 'string' &&
148
- typeof payload?.taskId === 'string' &&
149
- typeof payload?.status === 'string') {
150
- callback({ chatId: payload.chatId, taskId: payload.taskId, status: payload.status });
147
+ if (typeof payload?.chatId === 'string' && typeof payload?.taskId === 'string') {
148
+ callback({ chatId: payload.chatId, taskId: payload.taskId });
151
149
  }
152
150
  };
153
151
  ipcRenderer.on('task-updated', handler);
package/dist/runtime.js CHANGED
@@ -124,8 +124,12 @@ export function resolveProductWebDist(entryUrl) {
124
124
  const here = path.dirname(fileURLToPath(entryUrl));
125
125
  const productId = path.basename(here);
126
126
  for (const candidate of [
127
+ // 源码平面:入口在 products/<id>/(web dist 在同级 web/dist)。
127
128
  path.join(here, 'web', 'dist'),
128
- path.resolve(here, '..', '..', '..', '..', 'products', productId, 'web', 'dist'),
129
+ // 编译平面 A:入口在仓根 dist/products/<id>/(上溯 3 级到仓根)。
130
+ path.resolve(here, '..', '..', '..', 'products', productId, 'web', 'dist'),
131
+ // 编译平面 B:入口在产品内 products/<id>/dist/products/<id>/(上溯 5 级到仓根)。
132
+ path.resolve(here, '..', '..', '..', '..', '..', 'products', productId, 'web', 'dist'),
129
133
  ]) {
130
134
  if (fs.existsSync(path.join(candidate, 'index.html')))
131
135
  return candidate;
@@ -12,9 +12,16 @@
12
12
  * (terminal / local / approval 为宿主自带;
13
13
  * 场景包路由经 host/http-routes 注册表匹配)。
14
14
  *
15
- * 安全约定:默认绑定 127.0.0.1、无认证——本机自用定位(与 Electron 版相同
16
- * 的信任模型:能到端口 = 本机用户)。不要绑到非回环地址,那会把 shell /
17
- * 文件读写 / 场景包工具控制暴露给整个网络。
15
+ * 安全约定:默认绑定 127.0.0.1。仅绑回环挡不住 DNS rebinding(恶意网站
16
+ * 重绑定后受害者浏览器即同源驱动本服务),所以有两道门:
17
+ * 1. Host 白名单——所有请求(含静态)的 Host 必须是 127.0.0.1 /
18
+ * localhost / [::1],浏览器无法伪造 Host,rebinding 在此被拒;
19
+ * 2. Bearer token——每次启动随机生成,经 index.html 注入
20
+ * `window.__DEEPPATH_BS__.token`;`/api/v2/*` 与 `/host/*` 全部要求
21
+ * `Authorization: Bearer <token>`(EventSource 不能设头,SSE 走
22
+ * `?token=` query)。静态资源不验 token(浏览器要靠它拿到 token)。
23
+ * 不要绑到非回环地址,那会把 shell / 文件读写 / 场景包工具控制暴露给整个
24
+ * 网络(token 只能挡没有 token 的人)。
18
25
  */
19
26
  import { type Server } from 'node:http';
20
27
  import type { LocalBackendRouter } from '../local-backend/router.js';
@@ -35,5 +42,7 @@ export interface BsServerDeps {
35
42
  bus: SseBus;
36
43
  /** apps/web/dist 的绝对路径。 */
37
44
  webDistDir: string;
45
+ /** Bearer token;缺省每次启动随机生成。测试注入固定值。 */
46
+ authToken?: string;
38
47
  }
39
48
  export declare function createBsServer(deps: BsServerDeps): Server;
@@ -12,11 +12,19 @@
12
12
  * (terminal / local / approval 为宿主自带;
13
13
  * 场景包路由经 host/http-routes 注册表匹配)。
14
14
  *
15
- * 安全约定:默认绑定 127.0.0.1、无认证——本机自用定位(与 Electron 版相同
16
- * 的信任模型:能到端口 = 本机用户)。不要绑到非回环地址,那会把 shell /
17
- * 文件读写 / 场景包工具控制暴露给整个网络。
15
+ * 安全约定:默认绑定 127.0.0.1。仅绑回环挡不住 DNS rebinding(恶意网站
16
+ * 重绑定后受害者浏览器即同源驱动本服务),所以有两道门:
17
+ * 1. Host 白名单——所有请求(含静态)的 Host 必须是 127.0.0.1 /
18
+ * localhost / [::1],浏览器无法伪造 Host,rebinding 在此被拒;
19
+ * 2. Bearer token——每次启动随机生成,经 index.html 注入
20
+ * `window.__DEEPPATH_BS__.token`;`/api/v2/*` 与 `/host/*` 全部要求
21
+ * `Authorization: Bearer <token>`(EventSource 不能设头,SSE 走
22
+ * `?token=` query)。静态资源不验 token(浏览器要靠它拿到 token)。
23
+ * 不要绑到非回环地址,那会把 shell / 文件读写 / 场景包工具控制暴露给整个
24
+ * 网络(token 只能挡没有 token 的人)。
18
25
  */
19
26
  import { createServer } from 'node:http';
27
+ import { randomBytes } from 'node:crypto';
20
28
  import { readFile } from 'node:fs/promises';
21
29
  import { existsSync } from 'node:fs';
22
30
  import path from 'node:path';
@@ -52,6 +60,8 @@ const MIME = {
52
60
  '.ttf': 'font/ttf',
53
61
  '.txt': 'text/plain; charset=utf-8',
54
62
  };
63
+ /** Host 白名单:仅回环(可带端口)。浏览器禁止伪造 Host,rebinding 到此为止。 */
64
+ const LOOPBACK_HOST = /^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/;
55
65
  function sendJson(res, status, data) {
56
66
  const body = JSON.stringify(data ?? null);
57
67
  res.writeHead(status, {
@@ -81,6 +91,13 @@ function asRecord(body) {
81
91
  }
82
92
  export function createBsServer(deps) {
83
93
  const { localBackendRouter, localExecutor, localScriptRegistry, terminalManager, approvalBridge, askUserBridge, maybeExecInTerminal, bus, webDistDir, } = deps;
94
+ const authToken = deps.authToken ?? randomBytes(24).toString('base64url');
95
+ /** `/api/v2/*` 与 `/host/*` 的 Bearer 门;EventSource 不能设头,收 query token。 */
96
+ function isAuthorized(req, url) {
97
+ if (req.headers.authorization === `Bearer ${authToken}`)
98
+ return true;
99
+ return url.searchParams.get('token') === authToken;
100
+ }
84
101
  async function handleApi(req, res, pathname) {
85
102
  const isStream = req.method === 'POST' && STREAM_PATH_PATTERNS.some((p) => p.test(pathname));
86
103
  const body = req.method === 'POST' || req.method === 'PATCH' || req.method === 'PUT'
@@ -315,6 +332,7 @@ export function createBsServer(deps) {
315
332
  platform: process.platform,
316
333
  flavor: brand.flavor,
317
334
  brandName: brand.displayName,
335
+ token: authToken,
318
336
  })};</script>`;
319
337
  content = content.toString('utf8').replace('<head>', `<head>${bootstrap}`);
320
338
  }
@@ -331,8 +349,21 @@ export function createBsServer(deps) {
331
349
  }
332
350
  return createServer((req, res) => {
333
351
  void (async () => {
352
+ // 门 1:Host 白名单(含静态——rebound 浏览器连 index.html 里的
353
+ // token 都不许拿到)。
354
+ if (!LOOPBACK_HOST.test(req.headers.host ?? '')) {
355
+ sendJson(res, 403, { detail: 'forbidden host' });
356
+ return;
357
+ }
334
358
  const url = new URL(req.url ?? '/', 'http://bs.local');
335
359
  const pathname = url.pathname;
360
+ // 门 2:API 与宿主能力要 Bearer token;静态资源不验(浏览器靠它
361
+ // 拿 token),静态面不暴露任何能力。
362
+ const needsAuth = pathname.startsWith('/api/v2/') || pathname.startsWith('/host/');
363
+ if (needsAuth && !isAuthorized(req, url)) {
364
+ sendJson(res, 401, { detail: 'unauthorized' });
365
+ return;
366
+ }
336
367
  if (pathname === '/api/v2/events' && req.method === 'GET') {
337
368
  bus.attach(res);
338
369
  return;
@@ -125,11 +125,24 @@ export class ToolRouter {
125
125
  },
126
126
  {
127
127
  name: 'local_read_file',
128
- description: 'Read a local text file',
128
+ description: 'Read a local text file. Large files can be paged with offset/limit (1-based line numbers); ' +
129
+ 'a paged or clipped result is marked `partial: true`, and a full-file overwrite of a path ' +
130
+ 'only partially read in this session is rejected — use local_edit_file for targeted changes, ' +
131
+ 'or finish reading all segments before rewriting.',
129
132
  mode: 'read',
130
133
  inputSchema: {
131
134
  type: 'object',
132
- properties: { path: { type: 'string' } },
135
+ properties: {
136
+ path: { type: 'string' },
137
+ offset: {
138
+ type: 'number',
139
+ description: 'Optional 1-based line number to start reading from (for paging large files).',
140
+ },
141
+ limit: {
142
+ type: 'number',
143
+ description: 'Optional maximum number of lines to return (for paging large files).',
144
+ },
145
+ },
133
146
  required: ['path'],
134
147
  },
135
148
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steerable/agent-shell",
3
- "version": "0.6.17",
3
+ "version": "0.6.19",
4
4
  "description": "Steerable framework — product-neutral desktop/headless agent host (Tier 5). Electron main + preload + headless HTTP server (BS) sharing one storage/sidecar/tooling core; scenario packs extend it through the @steerable/pack-sdk contract and are composed at build time by the consuming product.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://steerableframework.com/",
@@ -41,9 +41,9 @@
41
41
  "electron-store": "^8.1.0",
42
42
  "he": "^1.2.0",
43
43
  "node-pty": "^1.1.0",
44
- "@steerable/agent-harness": "0.6.17",
45
- "@steerable/pack-sdk": "0.6.17",
46
- "@steerable/agent-protocol": "0.6.17"
44
+ "@steerable/agent-harness": "0.6.19",
45
+ "@steerable/agent-protocol": "0.6.19",
46
+ "@steerable/pack-sdk": "0.6.19"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/better-sqlite3": "^7.6.13",