evolcore 0.0.35 → 0.0.37

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.
Files changed (224) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +6 -0
  3. package/bin/install-codex-managed-hooks.mjs +20 -3
  4. package/dist/agents/baseagent-availability.js +1 -1
  5. package/dist/agents/baseagent.js +88 -0
  6. package/dist/agents/claude-runner.js +59 -12
  7. package/dist/agents/codex/app-server-client.js +18 -1
  8. package/dist/agents/codex-runner.js +262 -21
  9. package/dist/agents/ecagent-runner.js +47 -11
  10. package/dist/agents/gemini-runner.js +36 -12
  11. package/dist/aun/aid/client.js +4 -3
  12. package/dist/aun/aid/control-aid.js +270 -8
  13. package/dist/aun/aid/identity.js +6 -3
  14. package/dist/aun/aid/ownership-operation.js +58 -0
  15. package/dist/aun/aid/store.js +1 -0
  16. package/dist/aun/group/fs-download.js +67 -2
  17. package/dist/aun/group/history.js +20 -5
  18. package/dist/aun/group/operations.js +8 -2
  19. package/dist/aun/menu/client.js +39 -2
  20. package/dist/aun/menu/transport.js +1 -1
  21. package/dist/aun/msg/history.js +43 -4
  22. package/dist/aun/msg/managed-operation.js +30 -53
  23. package/dist/aun/msg/p2p.js +51 -15
  24. package/dist/aun/outbox.js +4 -0
  25. package/dist/channels/aun.js +260 -73
  26. package/dist/channels/daemon.js +6 -4
  27. package/dist/channels/feishu.js +3 -0
  28. package/dist/cli/agent-command.js +161 -223
  29. package/dist/cli/agent-create.js +190 -223
  30. package/dist/cli/agent.js +3 -1
  31. package/dist/cli/aid-bind-command.js +53 -0
  32. package/dist/cli/aun-commands.js +384 -91
  33. package/dist/cli/bench.js +1 -1
  34. package/dist/cli/cli-argv.js +74 -0
  35. package/dist/cli/daemon-commands.js +105 -74
  36. package/dist/cli/diag.js +1 -1
  37. package/dist/cli/fs-command.js +18 -19
  38. package/dist/cli/getmeta-command.js +159 -6
  39. package/dist/cli/index.js +6 -12
  40. package/dist/cli/init-channel.js +8 -55
  41. package/dist/cli/init.js +125 -218
  42. package/dist/cli/queue-command.js +11 -8
  43. package/dist/cli/restart-monitor.js +177 -85
  44. package/dist/cli/session-command.js +1 -1
  45. package/dist/cli/stats.js +7 -5
  46. package/dist/cli/thread-command.js +103 -30
  47. package/dist/cli/trigger-command.js +1 -1
  48. package/dist/cli/watch-msg-diag.js +1 -0
  49. package/dist/config/access-policy.js +2 -2
  50. package/dist/config/boot-log.js +14 -11
  51. package/dist/config/config-field-policy.js +9 -1
  52. package/dist/config/config-manager.js +176 -90
  53. package/dist/config/config-operation-service.js +7 -14
  54. package/dist/config/contact-book-store.js +142 -26
  55. package/dist/config/contact-book.js +4 -2
  56. package/dist/config/lifecycle.js +1 -1
  57. package/dist/config/owner-config.js +96 -0
  58. package/dist/config/peer-role-resolver.js +12 -18
  59. package/dist/config/providers-config.js +382 -0
  60. package/dist/config/providers-import.js +74 -0
  61. package/dist/config/providers-management.js +328 -0
  62. package/dist/config/providers.js +22 -0
  63. package/dist/config/resolved-config-op.js +17 -14
  64. package/dist/config/snapshot.js +28 -16
  65. package/dist/config-store.js +18 -14
  66. package/dist/core/agent-ipc-registration.js +21 -63
  67. package/dist/core/agent-runtime.js +16 -5
  68. package/dist/core/auth/agent-delegation.js +199 -25
  69. package/dist/core/auth/auth-gateway.js +44 -2
  70. package/dist/core/auth/authorization-denial.js +1 -1
  71. package/dist/core/auth/debug-context.js +2 -1
  72. package/dist/core/auth/delegation-authorization.js +2 -2
  73. package/dist/core/auth/delegation-diagnostics.js +4 -1
  74. package/dist/core/auth/operation-authorizer.js +10 -1
  75. package/dist/core/auth/operation-catalog.js +19 -1
  76. package/dist/core/bootstrap-service.js +43 -20
  77. package/dist/core/channel-loader.js +0 -1
  78. package/dist/core/channel-runtime.js +30 -4
  79. package/dist/core/command/agent-control.js +196 -37
  80. package/dist/core/command/cli-intent-parser.js +60 -4
  81. package/dist/core/command/command-handler.js +212 -60
  82. package/dist/core/command/connect-menu.js +1 -0
  83. package/dist/core/command/diag-service.js +5 -4
  84. package/dist/core/command/file-search.js +368 -0
  85. package/dist/core/command/menu-catalog.js +2 -1
  86. package/dist/core/command/menu-handler.js +292 -107
  87. package/dist/core/command/menu-protocol.js +23 -0
  88. package/dist/core/command/role-menu.js +7 -19
  89. package/dist/core/command/skill-slash.js +130 -25
  90. package/dist/core/command/slash-handler.js +101 -53
  91. package/dist/core/command/topic-slash.js +16 -5
  92. package/dist/core/daemon-bootstrap.js +7 -1
  93. package/dist/core/daemon-control.js +19 -1
  94. package/dist/core/daemon-lifecycle.js +2 -1
  95. package/dist/core/event-catalog.js +2 -0
  96. package/dist/core/evolagent-registry.js +13 -2
  97. package/dist/core/evolagent.js +3 -6
  98. package/dist/core/handoff/ipc-registration.js +2 -2
  99. package/dist/core/handoff/runtime.js +40 -6
  100. package/dist/core/handoff/store.js +1 -0
  101. package/dist/core/inference/text-inference.js +95 -6
  102. package/dist/core/ipc-registration.js +43 -24
  103. package/dist/core/memory/consolidation-context.js +4 -1
  104. package/dist/core/memory/consolidation.js +25 -14
  105. package/dist/core/message/aun-ipc-sender.js +296 -88
  106. package/dist/core/message/aun-request-registry.js +226 -0
  107. package/dist/core/message/create-status.js +33 -4
  108. package/dist/core/message/message-bridge.js +62 -13
  109. package/dist/core/message/message-log.js +0 -1
  110. package/dist/core/message/message-queue.js +4 -1
  111. package/dist/core/message/message-utils.js +8 -1
  112. package/dist/core/message/peer-mode.js +17 -9
  113. package/dist/core/message/response-engine.js +266 -124
  114. package/dist/core/message/retry-scheduler.js +11 -8
  115. package/dist/core/model/catalog-service.js +60 -18
  116. package/dist/core/model/model-catalog.js +112 -9
  117. package/dist/core/model/model-fallback.js +94 -26
  118. package/dist/core/ownership/owner-resolver.js +687 -0
  119. package/dist/core/permission/approval-gateway.js +196 -62
  120. package/dist/core/permission/ec-command-parser.js +138 -25
  121. package/dist/core/permission/filesystem-approval-scope.js +141 -0
  122. package/dist/core/permission/protected-paths.js +14 -1
  123. package/dist/core/permission/sandbox-runtime.js +171 -18
  124. package/dist/core/permission/tool-policy.js +13 -2
  125. package/dist/core/role/runtime-policy.js +3 -4
  126. package/dist/core/session/session-idle-monitor.js +10 -8
  127. package/dist/core/session/session-manager.js +72 -45
  128. package/dist/core/session/session-mapper.js +18 -7
  129. package/dist/core/session/session-renew.js +5 -4
  130. package/dist/core/session/session-title.js +2 -1
  131. package/dist/core/session/session-turn-coordinator.js +4 -2
  132. package/dist/core/skill/index-generator.js +2 -16
  133. package/dist/core/task/task-recovery-service.js +15 -2
  134. package/dist/core/thread/command-parser.js +28 -13
  135. package/dist/core/thread/index.js +1 -0
  136. package/dist/core/thread/ipc-operation.js +11 -8
  137. package/dist/core/thread/thread-operation.js +79 -44
  138. package/dist/core/thread/thread-route.js +54 -6
  139. package/dist/core/thread/thread-service.js +236 -45
  140. package/dist/core/thread/topic-id.js +248 -0
  141. package/dist/eck/kit-renderer.js +1 -1
  142. package/dist/eck/message-renderer.js +12 -2
  143. package/dist/index.js +50 -27
  144. package/dist/ipc.js +59 -6
  145. package/dist/paths.js +1 -0
  146. package/dist/product.js +1 -0
  147. package/dist/response-system/engines/v1/proactive-flow.js +10 -3
  148. package/dist/stats/db.js +8 -0
  149. package/dist/stats/writer.js +26 -4
  150. package/dist/trigger/authorization.js +48 -3
  151. package/dist/trigger/feedback.js +28 -10
  152. package/dist/trigger/ipc-service.js +86 -10
  153. package/dist/trigger/local-files.js +55 -23
  154. package/dist/trigger/scheduler.js +57 -11
  155. package/dist/trigger/system-definitions.js +1 -0
  156. package/dist/trigger/validation.js +44 -15
  157. package/dist/utils/aid-bind.js +17 -297
  158. package/dist/utils/avatar-upload.js +16 -4
  159. package/dist/utils/cross-platform.js +115 -36
  160. package/dist/utils/ecweb-supervisor.js +1 -1
  161. package/dist/utils/ecweb-utils.js +8 -1
  162. package/dist/utils/error-utils.js +44 -6
  163. package/dist/utils/instance-registry.js +12 -2
  164. package/dist/utils/npm-ops.js +102 -27
  165. package/dist/utils/package-version-info.js +72 -27
  166. package/dist/utils/restart-safety.js +50 -0
  167. package/dist/utils/upgrade-jobs.js +304 -0
  168. package/dist/utils/windows-autostart.js +17 -10
  169. package/kits/docs/INDEX.md +5 -0
  170. package/kits/docs/aun/ABILITY_CALLING.md +239 -0
  171. package/kits/docs/evolcore/INDEX.md +14 -3
  172. package/kits/docs/evolcore/agent-create.md +55 -141
  173. package/kits/docs/evolcore/agent.md +38 -16
  174. package/kits/docs/evolcore/aid.md +34 -0
  175. package/kits/docs/evolcore/config.md +20 -1
  176. package/kits/docs/evolcore/event.md +2 -2
  177. package/kits/docs/evolcore/fs.md +18 -2
  178. package/kits/docs/evolcore/group-collaboration.md +13 -246
  179. package/kits/docs/evolcore/group.md +2 -1
  180. package/kits/docs/evolcore/memory-consolidation-batches.md +1 -1
  181. package/kits/docs/evolcore/menu-protocol.md +65 -1
  182. package/kits/docs/evolcore/msg.md +64 -2
  183. package/kits/docs/evolcore/skill.md +86 -0
  184. package/kits/docs/evolcore/stats.md +6 -0
  185. package/kits/docs/evolcore/task.md +2 -0
  186. package/kits/docs/evolcore/thread-chain.md +145 -51
  187. package/kits/docs/evolcore/trigger.md +48 -3
  188. package/kits/eck_manifest.json +21 -0
  189. package/kits/eck_message_manifest.json +4 -4
  190. package/kits/migrations/migrate-role-config-v5.mjs +20 -7
  191. package/kits/rules/01-overview.md +2 -1
  192. package/kits/rules/03-identity.md +3 -1
  193. package/kits/rules/04-relation.md +1 -1
  194. package/kits/rules/07-memory-and-thread.md +1 -1
  195. package/kits/schemas/_meta.json +16 -1
  196. package/kits/schemas/agent-config.schema.13.json +44 -2
  197. package/kits/schemas/contact-book.schema.3.json +5 -0
  198. package/kits/schemas/daemon.schema.6.json +3 -1
  199. package/kits/schemas/daemon.schema.8.json +329 -0
  200. package/kits/schemas/defaults.schema.5.json +39 -3
  201. package/kits/schemas/providers.schema.1.json +76 -0
  202. package/kits/schemas/relation-config.schema.9.json +5 -1
  203. package/kits/skills/.switches.json +28 -0
  204. package/kits/skills/aid-ability/SKILL.md +214 -0
  205. package/kits/skills/ec-agent-create/SKILL.md +162 -0
  206. package/kits/skills/ec-group/SKILL.md +2 -1
  207. package/kits/skills/ec-group-collaboration/SKILL.md +228 -0
  208. package/kits/skills/ec-identity/SKILL.md +143 -0
  209. package/kits/skills/ec-skill/scripts/cli.mjs +36 -3
  210. package/kits/skills/ec-skill/scripts/skill.mjs +202 -15
  211. package/kits/skills/ec-trigger/SKILL.md +167 -0
  212. package/kits/skills/ec-trigger/references/examples.md +153 -0
  213. package/kits/skills/thread-ops/SKILL.md +108 -420
  214. package/kits/templates/message-fragments/thread-context.md +1 -2
  215. package/kits/templates/message-fragments/trigger-thread-check.md +1 -1
  216. package/kits/templates/roles/member.json +1 -1
  217. package/kits/templates/roles/visitor.json +1 -0
  218. package/kits/templates/system-fragments/bootstrap.md +1 -0
  219. package/kits/templates/system-fragments/channel.md +2 -1
  220. package/kits/templates/system-fragments/commands.md +5 -4
  221. package/kits/templates/system-fragments/identity.md +2 -0
  222. package/kits/templates/system-fragments/session.md +1 -1
  223. package/kits/templates/system-fragments/venue.md +1 -1
  224. package/package.json +4 -3
@@ -2,6 +2,7 @@ import path from 'node:path';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { isValidAid } from '../../aun/aid/validation.js';
4
4
  import { atomicReadJson, atomicWriteJson } from '../../utils/atomic-write.js';
5
+ import { isMainSession } from '../thread/topic-id.js';
5
6
  import { remoteAccessService } from '../auth/remote-access-store.js';
6
7
  import { KeyedFairMutex } from '../handoff/mutex.js';
7
8
  export const DIAGNOSTIC_AGENT_AID = 'diag.agentid.cn';
@@ -291,7 +292,7 @@ export class DiagnosticSessionRouter {
291
292
  return diagnosticLocks.forKey(diagnosticLedgerPath(chatDir)).runExclusive(async () => {
292
293
  const all = await this.sessions.listSessions(channel, message.channelId, { archiveFilter: 'all' });
293
294
  const ledger = readDiagnosticLedger(chatDir);
294
- const find = (caseId) => all.find(s => !s.threadId && s.selfAID === selfAid
295
+ const find = (caseId) => all.find(s => isMainSession(s) && s.selfAID === selfAid
295
296
  && s.metadata?.diagnostic?.caseId === caseId && s.metadata.diagnostic.controlAid === event.controlAid);
296
297
  let record = ledger.cases[event.caseId];
297
298
  if (record && record.ownerAid !== event.ownerAid)
@@ -300,7 +301,7 @@ export class DiagnosticSessionRouter {
300
301
  let session = find(event.caseId);
301
302
  if (!session) {
302
303
  const legacy = this.sessions.getActiveSessionSync(channel, message.channelId, channelType, selfAid);
303
- if (legacy && !legacy.threadId && !legacy.metadata?.diagnostic) {
304
+ if (legacy && isMainSession(legacy) && !legacy.metadata?.diagnostic) {
304
305
  await this.sessions.patchSessionMetadata(legacy.id, { diagnosticStatus: 'cancelled' });
305
306
  session = legacy;
306
307
  }
@@ -341,7 +342,7 @@ export class DiagnosticSessionRouter {
341
342
  if (ledger.activeCaseId !== event.caseId)
342
343
  return { notice: '该诊断已被新诊断替代。' };
343
344
  const oldActive = this.sessions.getActiveSessionSync(channel, message.channelId, channelType, selfAid);
344
- if (!record.cleanupComplete && oldActive && !oldActive.threadId && !oldActive.metadata?.diagnostic) {
345
+ if (!record.cleanupComplete && oldActive && isMainSession(oldActive) && !oldActive.metadata?.diagnostic) {
345
346
  // Migration from the historical shared Control conversation. Owner
346
347
  // conversations and sessions belonging to other hosts are untouched.
347
348
  await this.sessions.patchSessionMetadata(oldActive.id, { diagnosticStatus: 'cancelled' });
@@ -349,7 +350,7 @@ export class DiagnosticSessionRouter {
349
350
  }
350
351
  // Retry interruption too after an interrupted creation attempt.
351
352
  for (const previous of record.cleanupComplete ? [] : all) {
352
- if (!previous.threadId && previous.metadata?.diagnostic?.controlAid === event.controlAid
353
+ if (isMainSession(previous) && previous.metadata?.diagnostic?.controlAid === event.controlAid
353
354
  && previous.metadata.diagnostic.caseId !== event.caseId)
354
355
  await this.stopSession(previous.id);
355
356
  }
@@ -0,0 +1,368 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { performance } from 'node:perf_hooks';
4
+ export const FILE_SEARCH_MAX_CONTENT_BYTES = 2 * 1024 * 1024;
5
+ export const FILE_SEARCH_MAX_RESPONSE_BYTES = 64 * 1024;
6
+ const DEFAULT_LIMIT = 100;
7
+ const MAX_LIMIT = 500;
8
+ const MAX_MATCHES = 3;
9
+ const MAX_SNIPPET_LENGTH = 200;
10
+ const EXCLUDED_DIRECTORIES = new Set(['.git', 'node_modules']);
11
+ export class FileSearchError extends Error {
12
+ code;
13
+ constructor(code, message) {
14
+ super(message);
15
+ this.code = code;
16
+ this.name = 'FileSearchError';
17
+ }
18
+ }
19
+ export function parseFileSearchArgs(value) {
20
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
21
+ throw new FileSearchError('INVALID_ARGUMENT', 'File search requires args');
22
+ }
23
+ const args = value;
24
+ if (Object.hasOwn(args, 'rootId')) {
25
+ throw new FileSearchError('INVALID_ARGUMENT', 'File search only supports the current project, not rootId');
26
+ }
27
+ if (args.path !== undefined && (typeof args.path !== 'string' || !args.path.trim()
28
+ || args.path.length > 4096 || args.path.includes('\0'))) {
29
+ throw new FileSearchError('INVALID_ARGUMENT', 'File search path must be a non-empty path');
30
+ }
31
+ for (const key of ['filename', 'content']) {
32
+ const query = args[key];
33
+ if (query !== undefined && (typeof query !== 'string' || !query.length || query.length > 1024
34
+ || query.includes('\0'))) {
35
+ throw new FileSearchError('INVALID_ARGUMENT', `File search ${key} must contain 1–1024 characters`);
36
+ }
37
+ }
38
+ for (const key of ['recursive', 'caseSensitive']) {
39
+ if (args[key] !== undefined && typeof args[key] !== 'boolean') {
40
+ throw new FileSearchError('INVALID_ARGUMENT', `File search ${key} must be a boolean`);
41
+ }
42
+ }
43
+ if (args.limit !== undefined && (!Number.isInteger(args.limit) || Number(args.limit) < 1
44
+ || Number(args.limit) > MAX_LIMIT)) {
45
+ throw new FileSearchError('INVALID_ARGUMENT', `File search limit must be an integer from 1 to ${MAX_LIMIT}`);
46
+ }
47
+ if (args.extensions !== undefined && (!Array.isArray(args.extensions) || args.extensions.length > 64)) {
48
+ throw new FileSearchError('INVALID_ARGUMENT', 'File search extensions must be an array of at most 64 extensions');
49
+ }
50
+ const extensions = new Set();
51
+ for (const extension of (args.extensions ?? [])) {
52
+ if (typeof extension !== 'string' || !/^\.?[^.\s\\/*?\0]{1,64}$/.test(extension)) {
53
+ throw new FileSearchError('INVALID_ARGUMENT', 'File search extensions must be literal suffixes such as ts or .json');
54
+ }
55
+ extensions.add(`.${extension.replace(/^\./, '').toLowerCase()}`);
56
+ }
57
+ if (args.filename === undefined && args.content === undefined && !extensions.size) {
58
+ throw new FileSearchError('INVALID_ARGUMENT', 'File search requires filename, content or extensions');
59
+ }
60
+ return {
61
+ path: args.path ?? '.',
62
+ recursive: args.recursive ?? true,
63
+ filename: args.filename,
64
+ content: args.content,
65
+ extensions: [...extensions],
66
+ caseSensitive: args.caseSensitive ?? false,
67
+ limit: args.limit ?? DEFAULT_LIMIT,
68
+ };
69
+ }
70
+ function isInside(root, candidate) {
71
+ const relative = path.relative(root, candidate);
72
+ return relative === '' || (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`));
73
+ }
74
+ function relativePath(root, candidate) {
75
+ return path.relative(root, candidate).split(path.sep).join('/') || '.';
76
+ }
77
+ function fileExtension(filename) {
78
+ const extension = path.extname(filename).toLowerCase();
79
+ if (extension)
80
+ return extension;
81
+ return filename.startsWith('.') && filename.indexOf('.', 1) < 0 ? filename.toLowerCase() : '';
82
+ }
83
+ async function searchDirectory(projectPath, input) {
84
+ if (input.split(/[\\/]/).includes('..') || (!path.isAbsolute(input) && path.win32.isAbsolute(input))) {
85
+ throw new FileSearchError('NO_PERMISSION', 'File search does not allow path traversal');
86
+ }
87
+ const logicalRoot = path.resolve(projectPath);
88
+ const root = await fs.promises.realpath(logicalRoot);
89
+ const candidate = path.resolve(logicalRoot, input);
90
+ const base = isInside(logicalRoot, candidate) ? logicalRoot : root;
91
+ if (!isInside(base, candidate)) {
92
+ throw new FileSearchError('NO_PERMISSION', 'File search must stay inside the current project');
93
+ }
94
+ let current = base;
95
+ for (const segment of path.relative(base, candidate).split(path.sep).filter(Boolean)) {
96
+ current = path.join(current, segment);
97
+ if ((await fs.promises.lstat(current)).isSymbolicLink()) {
98
+ throw new FileSearchError('NO_PERMISSION', 'File search does not follow symbolic links');
99
+ }
100
+ }
101
+ const start = await fs.promises.realpath(candidate);
102
+ if (!isInside(root, start)) {
103
+ throw new FileSearchError('NO_PERMISSION', 'File search must stay inside the current project');
104
+ }
105
+ if (!(await fs.promises.stat(start)).isDirectory()) {
106
+ throw new FileSearchError('NOT_A_DIRECTORY', 'File search path must be a directory');
107
+ }
108
+ return { root, start };
109
+ }
110
+ function contentMatches(text, query, caseSensitive) {
111
+ const lines = text.split(/\r\n|\r|\n/);
112
+ const comparedLines = caseSensitive ? lines : lines.map(line => line.toLowerCase());
113
+ const haystack = comparedLines.join('\n');
114
+ const normalizedQuery = query.replace(/\r\n|\r/g, '\n');
115
+ const needle = caseSensitive ? normalizedQuery : normalizedQuery.toLowerCase();
116
+ const matches = [];
117
+ let position = 0;
118
+ let lineIndex = 0;
119
+ let lineStart = 0;
120
+ let lineEnd = comparedLines[0].length;
121
+ while (matches.length < MAX_MATCHES) {
122
+ const found = haystack.indexOf(needle, position);
123
+ if (found < 0)
124
+ break;
125
+ while (found > lineEnd && lineIndex < lines.length - 1) {
126
+ lineStart = lineEnd + 1;
127
+ lineIndex++;
128
+ lineEnd = lineStart + comparedLines[lineIndex].length;
129
+ }
130
+ let column = found - lineStart;
131
+ if (!caseSensitive) {
132
+ let comparedColumn = 0;
133
+ let originalColumn = 0;
134
+ for (const character of lines[lineIndex]) {
135
+ if (comparedColumn >= column)
136
+ break;
137
+ comparedColumn += character.toLowerCase().length;
138
+ originalColumn += character.length;
139
+ }
140
+ column = originalColumn;
141
+ }
142
+ const snippetStart = Math.max(0, column - Math.floor(MAX_SNIPPET_LENGTH / 3));
143
+ matches.push({
144
+ line: lineIndex + 1,
145
+ text: lines[lineIndex]
146
+ .slice(snippetStart, snippetStart + MAX_SNIPPET_LENGTH)
147
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, '�'),
148
+ });
149
+ position = lineEnd + 1;
150
+ if (position > haystack.length)
151
+ break;
152
+ }
153
+ return matches;
154
+ }
155
+ async function readContent(filePath, expected, signal) {
156
+ const handle = await fs.promises.open(filePath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
157
+ try {
158
+ const opened = await handle.stat();
159
+ if (!opened.isFile() || opened.dev !== expected.dev || opened.ino !== expected.ino) {
160
+ return { skipped: 'unreadable' };
161
+ }
162
+ if (opened.size > FILE_SEARCH_MAX_CONTENT_BYTES)
163
+ return { skipped: 'tooLarge' };
164
+ const decoder = new TextDecoder('utf-8', { fatal: true });
165
+ const chunks = [];
166
+ let bytes = 0;
167
+ let controlBytes = 0;
168
+ const stream = handle.createReadStream({
169
+ autoClose: false, highWaterMark: 64 * 1024, end: FILE_SEARCH_MAX_CONTENT_BYTES, signal,
170
+ });
171
+ try {
172
+ for await (const chunk of stream) {
173
+ const buffer = chunk;
174
+ bytes += buffer.length;
175
+ if (bytes > FILE_SEARCH_MAX_CONTENT_BYTES)
176
+ return { skipped: 'tooLarge' };
177
+ if (buffer.includes(0))
178
+ return { skipped: 'binary' };
179
+ for (const byte of buffer) {
180
+ if ((byte < 32 && byte !== 9 && byte !== 10 && byte !== 12 && byte !== 13) || byte === 127)
181
+ controlBytes++;
182
+ }
183
+ try {
184
+ chunks.push(decoder.decode(buffer, { stream: true }));
185
+ }
186
+ catch {
187
+ return { skipped: 'unsupportedEncoding' };
188
+ }
189
+ }
190
+ try {
191
+ chunks.push(decoder.decode());
192
+ }
193
+ catch {
194
+ return { skipped: 'unsupportedEncoding' };
195
+ }
196
+ if (bytes > 0 && controlBytes / bytes > 0.1)
197
+ return { skipped: 'binary' };
198
+ const stat = await handle.stat();
199
+ if (stat.size > FILE_SEARCH_MAX_CONTENT_BYTES)
200
+ return { skipped: 'tooLarge' };
201
+ return { text: chunks.join(''), stat };
202
+ }
203
+ finally {
204
+ stream.destroy();
205
+ }
206
+ }
207
+ finally {
208
+ await handle.close();
209
+ }
210
+ }
211
+ export async function searchProjectFiles(projectPath, value, options = {}) {
212
+ const args = parseFileSearchArgs(value);
213
+ if (!projectPath)
214
+ throw new FileSearchError('NO_PROJECT', 'Unable to resolve the current Agent projectPath');
215
+ const { timeoutMs = 5000, maxEntries = 10000, maxResponseBytes = FILE_SEARCH_MAX_RESPONSE_BYTES } = options;
216
+ const controller = new AbortController();
217
+ const deadline = performance.now() + timeoutMs;
218
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
219
+ timer.unref();
220
+ try {
221
+ const { root, start } = await searchDirectory(projectPath, args.path);
222
+ const result = {
223
+ path: relativePath(root, start), entries: [], count: 0, scannedFiles: 0,
224
+ truncated: false, truncatedReason: null,
225
+ skipped: { binary: 0, unsupportedEncoding: 0, tooLarge: 0, unreadable: 0, symlink: 0 },
226
+ };
227
+ const stop = (reason) => {
228
+ result.truncated = true;
229
+ result.truncatedReason = reason;
230
+ };
231
+ const outOfTime = () => {
232
+ if (controller.signal.aborted || performance.now() >= deadline) {
233
+ stop('time_limit');
234
+ controller.abort();
235
+ return true;
236
+ }
237
+ return false;
238
+ };
239
+ let visited = 0;
240
+ let responseBytes = Buffer.byteLength(JSON.stringify(result)) + 512;
241
+ const directories = [start];
242
+ const filename = args.caseSensitive ? args.filename : args.filename?.toLowerCase();
243
+ while (directories.length && !result.truncated && !outOfTime()) {
244
+ const directory = directories.pop();
245
+ try {
246
+ if (await fs.promises.realpath(directory) !== directory) {
247
+ result.skipped.symlink++;
248
+ continue;
249
+ }
250
+ const entries = await fs.promises.opendir(directory);
251
+ for await (const entry of entries) {
252
+ if (outOfTime())
253
+ break;
254
+ if (visited++ >= maxEntries) {
255
+ stop('file_limit');
256
+ break;
257
+ }
258
+ const fullPath = path.join(directory, entry.name);
259
+ try {
260
+ const stat = await fs.promises.lstat(fullPath);
261
+ if (options.canAccessFile
262
+ && !await options.canAccessFile(relativePath(root, fullPath), fullPath, stat.isDirectory())) {
263
+ continue;
264
+ }
265
+ if (outOfTime())
266
+ break;
267
+ if (stat.isSymbolicLink()) {
268
+ result.skipped.symlink++;
269
+ continue;
270
+ }
271
+ if (stat.isDirectory()) {
272
+ if (args.recursive && !EXCLUDED_DIRECTORIES.has(entry.name)) {
273
+ const realDirectory = await fs.promises.realpath(fullPath);
274
+ if (realDirectory !== fullPath || !isInside(root, realDirectory)) {
275
+ result.skipped.symlink++;
276
+ continue;
277
+ }
278
+ directories.push(realDirectory);
279
+ }
280
+ continue;
281
+ }
282
+ if (!stat.isFile())
283
+ continue;
284
+ result.scannedFiles++;
285
+ const comparedName = args.caseSensitive ? entry.name : entry.name.toLowerCase();
286
+ if (filename !== undefined && !comparedName.includes(filename))
287
+ continue;
288
+ if (args.extensions.length && !args.extensions.includes(fileExtension(entry.name)))
289
+ continue;
290
+ if (await fs.promises.realpath(fullPath) !== fullPath || !isInside(root, fullPath)) {
291
+ result.skipped.symlink++;
292
+ continue;
293
+ }
294
+ let fileStat = stat;
295
+ let matches = [];
296
+ if (args.content !== undefined) {
297
+ if (stat.size > FILE_SEARCH_MAX_CONTENT_BYTES) {
298
+ result.skipped.tooLarge++;
299
+ continue;
300
+ }
301
+ const content = await readContent(fullPath, stat, controller.signal);
302
+ if (outOfTime())
303
+ break;
304
+ if ('skipped' in content) {
305
+ result.skipped[content.skipped]++;
306
+ continue;
307
+ }
308
+ matches = contentMatches(content.text, args.content, args.caseSensitive);
309
+ if (outOfTime())
310
+ break;
311
+ if (!matches.length)
312
+ continue;
313
+ fileStat = content.stat;
314
+ }
315
+ if (await fs.promises.realpath(fullPath) !== fullPath) {
316
+ result.skipped.symlink++;
317
+ continue;
318
+ }
319
+ if (outOfTime())
320
+ break;
321
+ const match = {
322
+ path: relativePath(root, fullPath), name: entry.name,
323
+ size: fileStat.size, mtime: fileStat.mtimeMs, matches,
324
+ };
325
+ const entryBytes = Buffer.byteLength(JSON.stringify(match)) + 1;
326
+ if (responseBytes + entryBytes > maxResponseBytes) {
327
+ stop('response_limit');
328
+ break;
329
+ }
330
+ result.entries.push(match);
331
+ result.count++;
332
+ responseBytes += entryBytes;
333
+ if (result.count >= args.limit) {
334
+ stop('result_limit');
335
+ break;
336
+ }
337
+ }
338
+ catch {
339
+ if (outOfTime())
340
+ break;
341
+ result.skipped.unreadable++;
342
+ }
343
+ }
344
+ }
345
+ catch {
346
+ if (outOfTime())
347
+ break;
348
+ if (directory === start)
349
+ throw new FileSearchError('NO_PERMISSION', 'Unable to read the search directory');
350
+ result.skipped.unreadable++;
351
+ }
352
+ }
353
+ return result;
354
+ }
355
+ catch (error) {
356
+ if (error instanceof FileSearchError)
357
+ throw error;
358
+ const code = error.code;
359
+ if (code === 'ENOENT')
360
+ throw new FileSearchError('NOT_FOUND', 'File search directory does not exist');
361
+ if (code === 'ENOTDIR')
362
+ throw new FileSearchError('NOT_A_DIRECTORY', 'File search path must be a directory');
363
+ throw new FileSearchError(code === 'EACCES' || code === 'EPERM' ? 'NO_PERMISSION' : 'EXEC_FAILED', 'Unable to search the current project');
364
+ }
365
+ finally {
366
+ clearTimeout(timer);
367
+ }
368
+ }
@@ -102,7 +102,8 @@ const ENTRIES = [
102
102
  { id: 'system.logs', type: 'list', contexts: ['process'], name: 'system', label: 'Daemon 日志', section: 'process', order: 12, query: { cmd: '/system', args: { view: 'logs' }, operation: 'system.status', scope: 'process' }, options: { cmd: '/system', args: { option: 'logs' }, operation: 'system.status', scope: 'process', mode: 'dynamic' }, control: 'list' },
103
103
  { id: 'config.rawFile', type: 'list', contexts: ['process'], name: 'config', label: '原始配置文件', section: 'process', order: 13, query: { cmd: '/config', args: { view: 'rawFile' }, operation: 'config.read', scope: 'process' }, options: { cmd: '/config', args: { option: 'rawFile' }, operation: 'config.read', scope: 'process', mode: 'dynamic' }, control: 'list' },
104
104
  { id: 'system.restart', type: 'action', contexts: ['process'], name: 'system', label: '重启服务', section: 'process', order: 20, action: { cmd: '/system', action: 'restart', operation: 'system.restart', scope: 'process' } },
105
- { id: 'system.upgrade', type: 'action', contexts: ['process'], name: 'system', label: '升级服务', section: 'process', order: 30, action: { cmd: '/system', action: 'upgrade', operation: 'system.upgrade', scope: 'process' } },
105
+ { id: 'system.upgrade', type: 'action', contexts: ['process'], name: 'system', label: '检查更新', section: 'process', order: 30, action: { cmd: '/system', action: 'upgrade', args: { mode: 'check' }, operation: 'system.upgrade', scope: 'process' } },
106
+ { id: 'system.upgrade.update', type: 'action', contexts: ['process'], name: 'system', label: '执行更新', section: 'process', order: 31, action: { cmd: '/system', action: 'upgrade', args: { mode: 'update' }, operation: 'system.upgrade', scope: 'process' } },
106
107
  { id: 'system.paircode', type: 'action', contexts: ['process'], name: 'system', label: '获取配对码', section: 'process', order: 35, action: { cmd: '/system', action: 'paircode', operation: 'system.paircode', scope: 'process' } },
107
108
  { id: 'agent.list', type: 'list', contexts: ['process'], name: 'agent', label: 'Agent 列表', section: 'process', order: 40, options: { cmd: '/agent', args: { options: 'all' }, operation: 'agent.list', scope: 'control', mode: 'dynamic' }, control: 'list' },
108
109
  { id: 'agent.create', type: 'action', contexts: ['process'], name: 'agent', label: '创建 Agent', section: 'process', order: 50, action: { cmd: '/agent', action: 'create', operation: 'agent.create', scope: 'control' } },