foliko 1.1.89 → 1.1.90

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foliko",
3
- "version": "1.1.89",
3
+ "version": "1.1.90",
4
4
  "description": "简约的插件化 Agent 框架",
5
5
  "main": "src/index.js",
6
6
  "type": "commonjs",
@@ -14,7 +14,13 @@ const bootstrapLog = logger.child('Bootstrap')
14
14
  * 加载 .foliko 目录下的配置
15
15
  * 返回配置对象,可用于手动加载插件
16
16
  */
17
- function loadAgentConfig(agentDir = '.foliko') {
17
+ function loadAgentConfig(framework, agentDir = '.foliko') {
18
+ // 向后兼容:旧调用 loadAgentConfig('.foliko') 时把 framework 当成 agentDir
19
+ if (typeof framework === 'string') {
20
+ agentDir = framework;
21
+ framework = null;
22
+ }
23
+
18
24
  const config = {
19
25
  agentDir,
20
26
  ai: {},
@@ -24,13 +30,9 @@ function loadAgentConfig(agentDir = '.foliko') {
24
30
  agentsDir: null, // 子Agent配置目录
25
31
  pluginLinks: {} // 外部插件链接 { "插件名": "外部路径" }
26
32
  }
27
- const cmdDir=path.resolve(process.cwd())
28
- const resolvedDir = path.resolve(process.cwd(), agentDir)
29
-
30
- if (!fs.existsSync(resolvedDir)) {
31
- //log.info(` .foliko directory not found: ${resolvedDir}`)
32
- return config
33
- }
33
+ const cwd = framework?.getCwd?.() ?? process.cwd()
34
+ const cmdDir = path.resolve(cwd)
35
+ const resolvedDir = path.resolve(cwd, agentDir)
34
36
 
35
37
  //log.info(` Loading config from: ${resolvedDir}`)
36
38
 
@@ -146,10 +148,10 @@ function loadAgentConfig(agentDir = '.foliko') {
146
148
 
147
149
  // 添加 .foliko/skills 目录(不存在则创建)
148
150
  const cmdskillsDir = path.join(cmdDir, 'skills')
149
- if (fs.existsSync(resolvedDir) && !fs.existsSync(cmdskillsDir)) {
150
- fs.mkdirSync(cmdskillsDir, { recursive: true })
151
- //log.info(` Created skills directory: ${cmdskillsDir}`)
152
- }
151
+ // if (fs.existsSync(resolvedDir) && !fs.existsSync(cmdskillsDir)) {
152
+ // fs.mkdirSync(cmdskillsDir, { recursive: true })
153
+ // //log.info(` Created skills directory: ${cmdskillsDir}`)
154
+ // }
153
155
  if (fs.existsSync(cmdskillsDir)) {
154
156
  config.skillsDirs.push(cmdskillsDir)
155
157
  }
@@ -167,6 +169,114 @@ function loadAgentConfig(agentDir = '.foliko') {
167
169
  // config.skillsDirs.push(defaultSkillsDir)
168
170
  // }
169
171
 
172
+ // ============ HOME DIR (~/.foliko/) SCAN ============
173
+ // Home 是独立配置源:
174
+ // - 单文件配置:项目不存在时 home 作为回退
175
+ // - 多文件资源(skills/agents/plugins):两个目录都扫描
176
+ const homeDir = framework?.getHomeAgentDir?.()
177
+ const isHomeSameAsProject = homeDir && path.resolve(homeDir) === resolvedDir
178
+ if (homeDir && fs.existsSync(homeDir) && !isHomeSameAsProject) {
179
+ // 单文件配置回退
180
+ const homeConfig = path.join(homeDir, 'config')
181
+ if (!fs.existsSync(path.join(resolvedDir, 'config')) && fs.existsSync(homeConfig)) {
182
+ try {
183
+ const lines = fs.readFileSync(homeConfig, 'utf-8').split('\n')
184
+ for (const line of lines) {
185
+ const trimmed = line.trim()
186
+ if (!trimmed || trimmed.startsWith('#')) continue
187
+ const colonIndex = trimmed.indexOf(':')
188
+ if (colonIndex === -1) continue
189
+ const key = trimmed.substring(0, colonIndex).trim()
190
+ const value = trimmed.substring(colonIndex + 1).trim()
191
+ if (key === 'ai_key' || key === 'api_key') {
192
+ if (!config.ai.apiKey) config.ai.apiKey = value
193
+ } else if (key === 'ai_model' || key === 'model') {
194
+ if (!config.ai.model) config.ai.model = value
195
+ } else if (key === 'ai_provider' || key === 'provider') {
196
+ if (!config.ai.provider) config.ai.provider = value
197
+ } else if (key === 'ai_base_url' || key === 'baseURL') {
198
+ if (!config.ai.baseURL) config.ai.baseURL = value
199
+ }
200
+ }
201
+ } catch (err) {
202
+ log.error(' Failed to load home config:', err.message)
203
+ }
204
+ }
205
+
206
+ const homeAi = path.join(homeDir, 'ai.json')
207
+ if (!fs.existsSync(path.join(resolvedDir, 'ai.json')) && fs.existsSync(homeAi)) {
208
+ try {
209
+ const homeObj = JSON.parse(fs.readFileSync(homeAi, 'utf-8'))
210
+ config.ai = { ...homeObj, ...config.ai }
211
+ } catch (err) {
212
+ log.error(' Failed to load home ai.json:', err.message)
213
+ }
214
+ }
215
+
216
+ const homePluginsFile = path.join(homeDir, 'plugins.json')
217
+ if (!fs.existsSync(path.join(resolvedDir, 'plugins.json')) && fs.existsSync(homePluginsFile)) {
218
+ try {
219
+ const pc = JSON.parse(fs.readFileSync(homePluginsFile, 'utf-8'))
220
+ if (pc.telegram && !config.telegram) config.telegram = pc.telegram
221
+ if (pc.weixin && !config.weixin) config.weixin = pc.weixin
222
+ if (pc.email && !config.email) config.email = pc.email
223
+ config.pluginLinks = { ...(pc.pluginLinks || {}), ...config.pluginLinks }
224
+ } catch (err) {
225
+ log.error(' Failed to load home plugins.json:', err.message)
226
+ }
227
+ }
228
+
229
+ const homeWeixin = path.join(homeDir, 'weixin.json')
230
+ if (!fs.existsSync(path.join(resolvedDir, 'weixin.json')) && fs.existsSync(homeWeixin)) {
231
+ try {
232
+ const w = JSON.parse(fs.readFileSync(homeWeixin, 'utf-8'))
233
+ config.weixin = { ...w, ...config.weixin }
234
+ } catch (err) {
235
+ log.error(' Failed to load home weixin.json:', err.message)
236
+ }
237
+ }
238
+
239
+ const homeMcp = path.join(homeDir, 'mcp_config.json')
240
+ if (!fs.existsSync(path.join(resolvedDir, 'mcp_config.json')) && fs.existsSync(homeMcp)) {
241
+ try {
242
+ const m = JSON.parse(fs.readFileSync(homeMcp, 'utf-8'))
243
+ config.mcpServers = { ...(m.mcpServers || {}), ...config.mcpServers }
244
+ } catch (err) {
245
+ log.error(' Failed to load home mcp_config.json:', err.message)
246
+ }
247
+ }
248
+
249
+ // agents 目录:home 仅在项目无 agentsDir 时生效
250
+ const homeAgentsDir = path.join(homeDir, 'agents')
251
+ if (!config.agentsDir && fs.existsSync(homeAgentsDir)) {
252
+ config.agentsDir = homeAgentsDir
253
+ }
254
+
255
+ // skills 目录:项目 .foliko/skills 之后插入 home
256
+ const homeSkillsDir = path.join(homeDir, 'skills')
257
+ if (!fs.existsSync(homeSkillsDir)) {
258
+ try { fs.mkdirSync(homeSkillsDir, { recursive: true }) } catch (err) { /* ignore */ }
259
+ }
260
+ if (fs.existsSync(homeSkillsDir)) {
261
+ const projectFolikoSkillsIdx = config.skillsDirs.findIndex(
262
+ d => d === path.join(resolvedDir, 'skills')
263
+ )
264
+ const insertAt = projectFolikoSkillsIdx >= 0
265
+ ? projectFolikoSkillsIdx + 1
266
+ : config.skillsDirs.length
267
+ config.skillsDirs.splice(insertAt, 0, homeSkillsDir)
268
+ }
269
+
270
+ // home plugins:扫描并加入 pluginLinks
271
+ const homePluginsDir = path.join(homeDir, 'plugins')
272
+ if (fs.existsSync(homePluginsDir)) {
273
+ for (const name of scanPluginNames(homePluginsDir)) {
274
+ const resolvedPlugin = resolvePluginPath(homePluginsDir, name)
275
+ if (resolvedPlugin) config.pluginLinks[name] = resolvedPlugin.path
276
+ }
277
+ }
278
+ }
279
+
170
280
  return config
171
281
  }
172
282
 
@@ -194,7 +304,7 @@ class DefaultPlugins extends Plugin {
194
304
 
195
305
  start(framework) {
196
306
  // 加载配置
197
- this._config = loadAgentConfig(this._agentDir)
307
+ this._config = loadAgentConfig(this._framework, this._agentDir)
198
308
  return this
199
309
  }
200
310
 
@@ -202,12 +312,12 @@ class DefaultPlugins extends Plugin {
202
312
  * 获取加载的配置
203
313
  */
204
314
  getConfig() {
205
- return this._config || loadAgentConfig(this._agentDir)
315
+ return this._config || loadAgentConfig(this._framework, this._agentDir)
206
316
  }
207
317
 
208
318
  reload(framework) {
209
319
  this._framework = framework
210
- this._config = loadAgentConfig(this._agentDir)
320
+ this._config = loadAgentConfig(this._framework, this._agentDir)
211
321
  }
212
322
 
213
323
  uninstall(framework) {
@@ -510,15 +620,17 @@ function scanPluginNames(pluginsDir) {
510
620
  async function loadCustomPlugins(framework, agentConfig) {
511
621
  const { Plugin } = require('../src/core/plugin-base')
512
622
 
623
+ const cwd = framework?.getCwd?.() ?? process.cwd()
624
+
513
625
  // 加载两个目录下的自定义插件:
514
626
  // 1. .foliko/plugins/ - 用户自定义插件(强制启用)
515
627
  // 2. plugins/ - 项目内置插件(自动加载)
516
628
  const dirs = [
517
- { dir: path.resolve(process.cwd(), '.foliko', 'plugins'), forceEnabled: true },
518
- { dir: path.resolve(process.cwd(), 'plugins'), forceEnabled: false },
519
- { dir: path.resolve(__dirname, '..', 'plugins'), forceEnabled: false }
629
+ { dir: path.resolve(cwd, '.foliko', 'plugins'), forceEnabled: true },
630
+ { dir: path.resolve(cwd, 'plugins'), forceEnabled: false },
631
+ { dir: path.resolve(__dirname, '..', 'plugins'), forceEnabled: false }
520
632
  // 项目下的 plugins 目录(兼容旧版本)
521
-
633
+
522
634
  ]
523
635
 
524
636
  for (const { dir, forceEnabled } of dirs) {
@@ -262,7 +262,7 @@ class FeishuPlugin extends Plugin {
262
262
  const agent = this._framework.createSessionAgent(`feishu_${openId}`, {
263
263
  systemPrompt: this.systemPrompt,
264
264
  sharedPrompt: `工作目录: {{WORK_DIR}}`,
265
- metadata: { WORK_DIR: process.cwd() }
265
+ metadata: { WORK_DIR: this._framework?.getCwd?.() ?? process.cwd() }
266
266
  })
267
267
  this._sessionAgents.set(openId, agent)
268
268
 
@@ -26,7 +26,7 @@ class FileSystemPlugin extends Plugin {
26
26
  // 路径安全验证:防止路径穿越攻击
27
27
  const validatePath = (filePath, allowOutsideCwd = false) => {
28
28
  const resolved = path.resolve(filePath)
29
- const cwd = process.cwd()
29
+ const cwd = framework?.getCwd?.() ?? process.cwd()
30
30
 
31
31
  // 允许绝对路径且在允许列表中的路径(如果有的话)
32
32
  if (allowOutsideCwd) {
@@ -51,6 +51,7 @@ class FileSystemPlugin extends Plugin {
51
51
  recursive: z.boolean().optional().describe('是否递归')
52
52
  }),
53
53
  execute: async (args, framework) => {
54
+ const cwd = framework?.getCwd?.() ?? process.cwd()
54
55
  const dirPath = args.path || args.dirPath || '.'
55
56
  const recursive = args.recursive || false
56
57
  try {
@@ -60,7 +61,7 @@ class FileSystemPlugin extends Plugin {
60
61
  const entries = fs.readdirSync(currentPath, { withFileTypes: true })
61
62
  for (const entry of entries) {
62
63
  const fullPath = path.join(currentPath, entry.name)
63
- const relativePath = path.relative(process.cwd(), fullPath)
64
+ const relativePath = path.relative(cwd, fullPath)
64
65
  if (relativePath.includes('node_modules') || relativePath.includes('.git')) {
65
66
  continue
66
67
  }
@@ -476,7 +477,7 @@ edits 数组可以包含多个不重叠的替换操作,每个操作通过 oldT
476
477
  execute: async (args, framework) => {
477
478
  const { path: filePath, edits } = args;
478
479
  const editDiff = require('../src/utils/edit-diff');
479
- const result = await editDiff.applyEditsToFile(filePath, edits, process.cwd());
480
+ const result = await editDiff.applyEditsToFile(filePath, edits, framework?.getCwd?.() ?? process.cwd());
480
481
  return result;
481
482
  }
482
483
  };
@@ -509,7 +510,7 @@ edits 数组可以包含多个不重叠的替换操作,每个操作通过 oldT
509
510
  }),
510
511
  execute: async (args, framework) => {
511
512
  const pattern = args.pattern
512
- const dirPath = args.path || process.cwd()
513
+ const dirPath = args.path || (framework?.getCwd?.() ?? process.cwd())
513
514
  const targetFile = args.file
514
515
  const fileType = args.fileType
515
516
  const maxResults = args.maxResults || 100
@@ -625,7 +626,7 @@ edits 数组可以包含多个不重叠的替换操作,每个操作通过 oldT
625
626
  if (results.length >= maxResults) break
626
627
 
627
628
  const fullPath = path.join(currentPath, entry.name)
628
- const relativePath = path.relative(process.cwd(), fullPath)
629
+ const relativePath = path.relative(cwd, fullPath)
629
630
 
630
631
  // 检查是否在排除目录中
631
632
  const shouldExclude = excludeDirs.some(exclude =>
@@ -696,7 +697,7 @@ edits 数组可以包含多个不重叠的替换操作,每个操作通过 oldT
696
697
  }),
697
698
  execute: async (args, framework) => {
698
699
  const command = args.cmd || args.command || args.run
699
- const cwd = args.cwd || process.cwd()
700
+ const cwd = args.cwd || (framework?.getCwd?.() ?? process.cwd())
700
701
  const timeout = Math.min(args.timeout || 30000, 120000) // 最多 2 分钟
701
702
 
702
703
  // 验证命令:检查危险的 shell 模式
@@ -762,7 +763,7 @@ edits 数组可以包含多个不重叠的替换操作,每个操作通过 oldT
762
763
  }),
763
764
  execute: async (args, framework) => {
764
765
  const command = args.cmd || args.command
765
- const cwd = args.cwd || process.cwd()
766
+ const cwd = args.cwd || (framework?.getCwd?.() ?? process.cwd())
766
767
  const timeout = Math.min(args.timeout || 30000, 120000)
767
768
 
768
769
  if (!command) {
@@ -30,6 +30,8 @@ class InstallPlugin extends Plugin {
30
30
 
31
31
  start(framework) {
32
32
  // 确保 node_modules 目录存在
33
+ const cwd = this._framework?.getCwd?.() ?? process.cwd()
34
+ this._agentDir = path.resolve(cwd, this._agentDir)
33
35
  this._nodeModulesDir = path.join(this._agentDir, 'node_modules')
34
36
  if (!fs.existsSync(this._nodeModulesDir)) {
35
37
  fs.mkdirSync(this._nodeModulesDir, { recursive: true })
@@ -46,10 +48,11 @@ class InstallPlugin extends Plugin {
46
48
  }),
47
49
  execute: async (args) => {
48
50
  const { package: packageName, path: targetPath, file: packageJsonPath } = args
51
+ const cwd = this._framework?.getCwd?.() ?? process.cwd()
49
52
 
50
53
  // 优先处理 file 参数(从 package.json 安装)
51
54
  if (packageJsonPath) {
52
- const resolvedPath = path.resolve(process.cwd(), packageJsonPath)
55
+ const resolvedPath = path.resolve(cwd, packageJsonPath)
53
56
  return this._installFromPackageJson(resolvedPath, targetPath)
54
57
  }
55
58
 
@@ -60,7 +63,7 @@ class InstallPlugin extends Plugin {
60
63
 
61
64
  // 只有 path,安装该目录的 package.json
62
65
  if (targetPath) {
63
- const resolvedPath = path.resolve(process.cwd(), targetPath)
66
+ const resolvedPath = path.resolve(cwd, targetPath)
64
67
  const pkgJson = path.join(resolvedPath, 'package.json')
65
68
  if (fs.existsSync(pkgJson)) {
66
69
  return this._installFromPackageJson(pkgJson, targetPath)
@@ -68,7 +71,7 @@ class InstallPlugin extends Plugin {
68
71
  return { success: false, error: `package.json not found at ${pkgJson}` }
69
72
  }
70
73
 
71
- // 什么都不提供,报错
74
+ // 都不提供,报错
72
75
  return { success: false, error: 'Must provide package name or package.json path' }
73
76
  }
74
77
  })
@@ -83,8 +86,9 @@ class InstallPlugin extends Plugin {
83
86
  */
84
87
  _installPackage(packageName, targetPath = null) {
85
88
  try {
89
+ const cwd = this._framework?.getCwd?.() ?? process.cwd()
86
90
  const installPath = targetPath
87
- ? path.resolve(process.cwd(), targetPath)
91
+ ? path.resolve(cwd, targetPath)
88
92
  : this._agentDir
89
93
 
90
94
  //log.info(` Installing ${packageName} to ${installPath}...`)
@@ -121,10 +125,11 @@ class InstallPlugin extends Plugin {
121
125
  */
122
126
  _installFromPackageJson(packageJsonPath, targetPath = null) {
123
127
  try {
124
- const resolvedPkgPath = path.resolve(process.cwd(), packageJsonPath)
128
+ const cwd = this._framework?.getCwd?.() ?? process.cwd()
129
+ const resolvedPkgPath = path.resolve(cwd, packageJsonPath)
125
130
  const pkgDir = path.dirname(resolvedPkgPath)
126
131
  const installPath = targetPath
127
- ? path.resolve(process.cwd(), targetPath)
132
+ ? path.resolve(cwd, targetPath)
128
133
  : pkgDir
129
134
 
130
135
  //log.info(` Installing dependencies from ${resolvedPkgPath} to ${installPath}...`)
@@ -210,9 +210,9 @@ function _extractJSON(text) {
210
210
  * MemoryStore - 双层存储(内存 + 文件)
211
211
  */
212
212
  class MemoryStore extends EventEmitter {
213
- constructor(baseDir) {
213
+ constructor(baseDir, cwd) {
214
214
  super()
215
- this._baseDir = path.resolve(process.cwd(), baseDir)
215
+ this._baseDir = path.resolve(cwd ?? process.cwd(), baseDir)
216
216
  this._memory = new Map() // memoryId -> memory object
217
217
  this._indexByType = new Map() // type -> Set of memoryIds
218
218
  this._indexByProject = new Map() // project -> Set of memoryIds
@@ -573,7 +573,7 @@ class MemoryPlugin extends Plugin {
573
573
 
574
574
  start(framework) {
575
575
  // 初始化存储
576
- this._store = new MemoryStore(this.config.memoryDir)
576
+ this._store = new MemoryStore(this.config.memoryDir, this._framework?.getCwd?.())
577
577
 
578
578
  // 创建复用的记忆提取子 Agent
579
579
  this._memoryAgent = framework.createSubAgent({
@@ -1276,7 +1276,7 @@ ${prompt}`
1276
1276
  reload(framework) {
1277
1277
  this._framework = framework
1278
1278
  // 重新初始化存储
1279
- this._store = new MemoryStore(this.config.memoryDir)
1279
+ this._store = new MemoryStore(this.config.memoryDir, this._framework?.getCwd?.())
1280
1280
  }
1281
1281
 
1282
1282
  uninstall(framework) {
@@ -212,8 +212,9 @@ class PluginManagerPlugin extends Plugin {
212
212
  * 发布插件到远程仓库
213
213
  */
214
214
  async _publishPlugin(pluginName, repo) {
215
- const pluginsDir = path.resolve(process.cwd(), '.foliko', 'plugins');
216
- const localPluginsDir = path.resolve(process.cwd(), 'plugins');
215
+ const cwd = this._framework?.getCwd?.() ?? process.cwd();
216
+ const pluginsDir = path.resolve(cwd, '.foliko', 'plugins');
217
+ const localPluginsDir = path.resolve(cwd, 'plugins');
217
218
 
218
219
  // 确定插件目录
219
220
  let actualPluginsDir = pluginsDir;
@@ -251,7 +252,7 @@ class PluginManagerPlugin extends Plugin {
251
252
 
252
253
  let isNewRepo = false;
253
254
  try {
254
- gitCommand(`clone ${repo} "${tmpDir}" --depth 1`, process.cwd());
255
+ gitCommand(`clone ${repo} "${tmpDir}" --depth 1`, this._framework?.getCwd?.() ?? process.cwd());
255
256
  } catch (err) {
256
257
  //log.info('Initializing new repository...');
257
258
  fs.mkdirSync(tmpDir, { recursive: true });
@@ -331,7 +332,8 @@ class PluginManagerPlugin extends Plugin {
331
332
  * 从远程仓库安装插件
332
333
  */
333
334
  async _installPlugin(pluginName, repo) {
334
- const localPluginsDir = path.resolve(process.cwd(), '.foliko', 'plugins');
335
+ const cwd = this._framework?.getCwd?.() ?? process.cwd();
336
+ const localPluginsDir = path.resolve(cwd, '.foliko', 'plugins');
335
337
  const tmpDir = path.join(require('os').tmpdir(), `foliko-plugin-install-${Date.now()}`);
336
338
 
337
339
  try {
@@ -339,7 +341,7 @@ class PluginManagerPlugin extends Plugin {
339
341
 
340
342
  // 克隆仓库
341
343
  fs.mkdirSync(tmpDir, { recursive: true });
342
- gitCommand(`clone ${repo} "${tmpDir}" --depth 1`, process.cwd());
344
+ gitCommand(`clone ${repo} "${tmpDir}" --depth 1`, cwd);
343
345
 
344
346
  // 查找插件
345
347
  const pluginDir = path.join(tmpDir, pluginName);
@@ -49,11 +49,7 @@ class QQPlugin extends Plugin {
49
49
  this.agent = null
50
50
  this.sessionId = null
51
51
 
52
- const saveDir = path.join(process.cwd(), '.foliko', 'data', this.name)
53
- this.downloader = new FileDownloader({
54
- retries: 3,
55
- baseDir: saveDir
56
- })
52
+ this.downloader = null
57
53
 
58
54
  this.systemPrompt = `你是一个 QQ 助手。
59
55
 
@@ -70,6 +66,13 @@ class QQPlugin extends Plugin {
70
66
 
71
67
  install(framework) {
72
68
  this._framework = framework
69
+ if (!this.downloader) {
70
+ const saveDir = path.join(this._framework?.getCwd?.() ?? process.cwd(), '.foliko', 'data', this.name)
71
+ this.downloader = new FileDownloader({
72
+ retries: 3,
73
+ baseDir: saveDir
74
+ })
75
+ }
73
76
  this._registerTools()
74
77
  return this
75
78
  }
@@ -527,7 +530,7 @@ class QQPlugin extends Plugin {
527
530
  name: `qq_${identifier}`,
528
531
  systemPrompt: this.systemPrompt,
529
532
  sharedPrompt: `工作目录: {{WORK_DIR}}`,
530
- metadata: { WORK_DIR: process.cwd(), platform: 'qq', identifier }
533
+ metadata: { WORK_DIR: this._framework?.getCwd?.() ?? process.cwd(), platform: 'qq', identifier }
531
534
  })
532
535
 
533
536
  this._sessionAgents.set(identifier, agent)
@@ -168,7 +168,7 @@ class RulesPlugin extends Plugin {
168
168
  * 从目录加载规则文件
169
169
  */
170
170
  loadFromDirectory(rulesDir) {
171
- const resolvedDir = path.resolve(process.cwd(), rulesDir)
171
+ const resolvedDir = path.resolve(this._framework?.getCwd?.() ?? process.cwd(), rulesDir)
172
172
 
173
173
  if (!fs.existsSync(resolvedDir)) {
174
174
  //log.info(` Rules directory not found: ${resolvedDir}`)
@@ -16,7 +16,7 @@ const { buildSessionContext, EntryTypes } = require('../src/core/session-entry')
16
16
  * Get default session directory for a cwd
17
17
  */
18
18
  function getDefaultSessionDir(cwd) {
19
- const agentDir = path.join(process.cwd(), '.foliko');
19
+ const agentDir = path.join(cwd, '.foliko');
20
20
  const sessionDir = path.join(agentDir, 'sessions');
21
21
  return sessionDir;
22
22
  }
@@ -54,10 +54,18 @@ class SessionPlugin extends Plugin {
54
54
  return this;
55
55
  }
56
56
 
57
+ /**
58
+ * 获取当前工作目录(framework.getCwd() 优先, fallback 到 process.cwd())
59
+ */
60
+ _defaultCwd() {
61
+ return this._framework?.getCwd?.() ?? process.cwd();
62
+ }
63
+
57
64
  /**
58
65
  * Get or create a SessionManager for a session
59
66
  */
60
- _getSessionManager(sessionId, cwd = process.cwd()) {
67
+ _getSessionManager(sessionId, cwd) {
68
+ cwd = cwd ?? this._defaultCwd();
61
69
  if (this._sessionManagers.has(sessionId)) {
62
70
  return this._sessionManagers.get(sessionId);
63
71
  }
@@ -72,7 +80,7 @@ class SessionPlugin extends Plugin {
72
80
  /**
73
81
  * List all sessions for current cwd
74
82
  */
75
- async listSessions(cwd = process.cwd()) {
83
+ async listSessions(cwd = this._defaultCwd()) {
76
84
  const sessionDir = getDefaultSessionDir(cwd);
77
85
  const sessions = [];
78
86
 
@@ -115,7 +123,7 @@ class SessionPlugin extends Plugin {
115
123
  cwd: z.string().optional().describe('工作目录')
116
124
  }),
117
125
  execute: async (args, fw) => {
118
- const cwd = args.cwd || process.cwd();
126
+ const cwd = args.cwd || this._defaultCwd();
119
127
  const sessionDir = getDefaultSessionDir(cwd);
120
128
  const manager = new SessionManager(cwd, sessionDir, undefined, true);
121
129
  const sessionId = manager.getSessionId();
@@ -140,7 +148,7 @@ class SessionPlugin extends Plugin {
140
148
  cwd: z.string().optional().describe('工作目录')
141
149
  }),
142
150
  execute: async (args) => {
143
- const cwd = args.cwd || process.cwd();
151
+ const cwd = args.cwd || this._defaultCwd();
144
152
  const manager = this._getSessionManager(args.sessionId, cwd);
145
153
  const entries = manager.getEntries();
146
154
 
@@ -163,7 +171,7 @@ class SessionPlugin extends Plugin {
163
171
  cwd: z.string().optional().describe('工作目录')
164
172
  }),
165
173
  execute: async (args) => {
166
- const cwd = args.cwd || process.cwd();
174
+ const cwd = args.cwd || this._defaultCwd();
167
175
  const sessions = await this.listSessions(cwd);
168
176
  return {
169
177
  success: true,
@@ -181,7 +189,7 @@ class SessionPlugin extends Plugin {
181
189
  cwd: z.string().optional().describe('工作目录')
182
190
  }),
183
191
  execute: async (args) => {
184
- const cwd = args.cwd || process.cwd();
192
+ const cwd = args.cwd || this._defaultCwd();
185
193
  const manager = this._getSessionManager(args.sessionId, cwd);
186
194
 
187
195
  if (manager.getSessionFile() && require('fs').existsSync(manager.getSessionFile())) {
@@ -203,7 +211,7 @@ class SessionPlugin extends Plugin {
203
211
  limit: z.number().optional().describe('返回消息数量限制')
204
212
  }),
205
213
  execute: async (args) => {
206
- const cwd = args.cwd || process.cwd();
214
+ const cwd = args.cwd || this._defaultCwd();
207
215
  const manager = this._getSessionManager(args.sessionId, cwd);
208
216
  const context = manager.buildSessionContext();
209
217
  let messages = context.messages;
@@ -233,7 +241,7 @@ class SessionPlugin extends Plugin {
233
241
  cwd: z.string().optional().describe('工作目录')
234
242
  }),
235
243
  execute: async (args) => {
236
- const cwd = args.cwd || process.cwd();
244
+ const cwd = args.cwd || this._defaultCwd();
237
245
  const manager = this._getSessionManager(args.sessionId, cwd);
238
246
 
239
247
  try {
@@ -262,7 +270,7 @@ class SessionPlugin extends Plugin {
262
270
  cwd: z.string().optional().describe('工作目录')
263
271
  }),
264
272
  execute: async (args) => {
265
- const cwd = args.cwd || process.cwd();
273
+ const cwd = args.cwd || this._defaultCwd();
266
274
  const manager = this._getSessionManager(args.sessionId, cwd);
267
275
 
268
276
  try {
@@ -307,7 +315,7 @@ class SessionPlugin extends Plugin {
307
315
  /**
308
316
  * Add message to session
309
317
  */
310
- addMessage(sessionId, message, cwd = process.cwd()) {
318
+ addMessage(sessionId, message, cwd = this._defaultCwd()) {
311
319
  const manager = this._getSessionManager(sessionId, cwd);
312
320
  const entryId = manager.appendMessage(message);
313
321
 
@@ -322,7 +330,7 @@ class SessionPlugin extends Plugin {
322
330
  /**
323
331
  * Get session context for LLM
324
332
  */
325
- getSessionContext(sessionId, cwd = process.cwd()) {
333
+ getSessionContext(sessionId, cwd = this._defaultCwd()) {
326
334
  const manager = this._getSessionManager(sessionId, cwd);
327
335
  return manager.buildSessionContext();
328
336
  }
@@ -330,14 +338,14 @@ class SessionPlugin extends Plugin {
330
338
  /**
331
339
  * Get or create session (alias for getSessionContext)
332
340
  */
333
- getOrCreateSession(sessionId, options = {}, cwd = process.cwd()) {
341
+ getOrCreateSession(sessionId, options = {}, cwd = this._defaultCwd()) {
334
342
  return this.getSessionContext(sessionId, cwd);
335
343
  }
336
344
 
337
345
  /**
338
346
  * Get specific entry
339
347
  */
340
- getEntry(sessionId, entryId, cwd = process.cwd()) {
348
+ getEntry(sessionId, entryId, cwd = this._defaultCwd()) {
341
349
  const manager = this._getSessionManager(sessionId, cwd);
342
350
  return manager.getEntry(entryId);
343
351
  }
@@ -345,7 +353,7 @@ class SessionPlugin extends Plugin {
345
353
  /**
346
354
  * Get branch (path from root to leaf)
347
355
  */
348
- getBranch(sessionId, fromId, cwd = process.cwd()) {
356
+ getBranch(sessionId, fromId, cwd = this._defaultCwd()) {
349
357
  const manager = this._getSessionManager(sessionId, cwd);
350
358
  return manager.getBranch(fromId);
351
359
  }
@@ -353,7 +361,7 @@ class SessionPlugin extends Plugin {
353
361
  /**
354
362
  * Append compaction entry
355
363
  */
356
- appendCompaction(sessionId, summary, firstKeptEntryId, tokensBefore, details, cwd = process.cwd()) {
364
+ appendCompaction(sessionId, summary, firstKeptEntryId, tokensBefore, details, cwd = this._defaultCwd()) {
357
365
  const manager = this._getSessionManager(sessionId, cwd);
358
366
  return manager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, false);
359
367
  }
@@ -361,7 +369,7 @@ class SessionPlugin extends Plugin {
361
369
  /**
362
370
  * Append thinking level change
363
371
  */
364
- appendThinkingLevelChange(sessionId, thinkingLevel, cwd = process.cwd()) {
372
+ appendThinkingLevelChange(sessionId, thinkingLevel, cwd = this._defaultCwd()) {
365
373
  const manager = this._getSessionManager(sessionId, cwd);
366
374
  return manager.appendThinkingLevelChange(thinkingLevel);
367
375
  }
@@ -369,7 +377,7 @@ class SessionPlugin extends Plugin {
369
377
  /**
370
378
  * Append model change
371
379
  */
372
- appendModelChange(sessionId, provider, modelId, cwd = process.cwd()) {
380
+ appendModelChange(sessionId, provider, modelId, cwd = this._defaultCwd()) {
373
381
  const manager = this._getSessionManager(sessionId, cwd);
374
382
  return manager.appendModelChange(provider, modelId);
375
383
  }
@@ -377,7 +385,7 @@ class SessionPlugin extends Plugin {
377
385
  /**
378
386
  * Set session label
379
387
  */
380
- setLabel(sessionId, entryId, label, cwd = process.cwd()) {
388
+ setLabel(sessionId, entryId, label, cwd = this._defaultCwd()) {
381
389
  const manager = this._getSessionManager(sessionId, cwd);
382
390
  return manager.appendLabelChange(entryId, label);
383
391
  }
@@ -385,7 +393,7 @@ class SessionPlugin extends Plugin {
385
393
  /**
386
394
  * Get session info (compatible with external callers)
387
395
  */
388
- getSession(sessionId, cwd = process.cwd()) {
396
+ getSession(sessionId, cwd = this._defaultCwd()) {
389
397
  const manager = this._getSessionManager(sessionId, cwd);
390
398
  const entries = manager.getEntries();
391
399
  return {
@@ -414,8 +422,9 @@ class SessionPlugin extends Plugin {
414
422
  async _doCleanup() {
415
423
  try {
416
424
  const fs = require('fs');
417
- const sessionDir = getDefaultSessionDir(process.cwd());
418
-
425
+ const cwd = this._defaultCwd();
426
+ const sessionDir = getDefaultSessionDir(cwd);
427
+
419
428
  if (!fs.existsSync(sessionDir)) {
420
429
  return;
421
430
  }
@@ -426,7 +435,7 @@ class SessionPlugin extends Plugin {
426
435
  for (const file of files) {
427
436
  const filePath = path.join(sessionDir, file);
428
437
  try {
429
- const manager = SessionManager.open(filePath, sessionDir, process.cwd());
438
+ const manager = SessionManager.open(filePath, sessionDir, cwd);
430
439
  const entries = manager.getEntries();
431
440
 
432
441
  // Clean up if exceeds maxHistoryLength
@@ -19,7 +19,7 @@ class ShellExecutorPlugin extends Plugin {
19
19
 
20
20
  this.config = {
21
21
  timeout: config.timeout || 60000,
22
- workingDir: config.workingDir || process.cwd()
22
+ workingDir: config.workingDir
23
23
  }
24
24
 
25
25
  this._framework = null
@@ -27,6 +27,9 @@ class ShellExecutorPlugin extends Plugin {
27
27
 
28
28
  install(framework) {
29
29
  this._framework = framework
30
+ if (!this.config.workingDir) {
31
+ this.config.workingDir = framework?.getCwd?.() ?? process.cwd()
32
+ }
30
33
  return this
31
34
  }
32
35
 
@@ -44,7 +44,7 @@ class StoragePlugin extends Plugin {
44
44
  _initStorage() {
45
45
  if (this._storageManager) return;
46
46
 
47
- const baseDir = path.resolve(process.cwd(), this.config.path);
47
+ const baseDir = path.resolve(this._framework?.getCwd?.() ?? process.cwd(), this.config.path);
48
48
  const filePath = path.join(baseDir, `${this.config.namespace}.jsonl`);
49
49
 
50
50
  // 传递 compaction 配置
@@ -587,7 +587,7 @@ class SubAgentManagerPlugin extends Plugin {
587
587
  */
588
588
  _loadAgentsFromDir() {
589
589
  // 默认使用 .foliko/agents 目录
590
- const agentsDir = this.config.agentsDir || path.join(process.cwd(), '.foliko', 'agents')
590
+ const agentsDir = this.config.agentsDir || path.join(this._framework?.getCwd?.() ?? process.cwd(), '.foliko', 'agents')
591
591
  //log.info(' _loadAgentsFromDir called, agentsDir:', agentsDir)
592
592
  if (!fs.existsSync(agentsDir)) {
593
593
  //log.info(' agentsDir not found or does not exist')
@@ -254,7 +254,7 @@ class TelegramPlugin extends Plugin {
254
254
  const agent = this._framework.createSessionAgent(`telegram_${chatId}`, {
255
255
  systemPrompt: this.systemPrompt,
256
256
  sharedPrompt: `工作目录: {{WORK_DIR}}`,
257
- metadata: { WORK_DIR: process.cwd() }
257
+ metadata: { WORK_DIR: this._framework?.getCwd?.() ?? process.cwd() }
258
258
  })
259
259
  this._sessionAgents.set(chatId, agent)
260
260
 
@@ -477,7 +477,7 @@ class TelegramPlugin extends Plugin {
477
477
  async _downloadFile(fileId, ext, subDir) {
478
478
  const path = require('path')
479
479
  const fs = require('fs')
480
- const saveDir = path.join(process.cwd(), '.foliko', 'data', subDir)
480
+ const saveDir = path.join(this._framework?.getCwd?.() ?? process.cwd(), '.foliko', 'data', subDir)
481
481
  if (!fs.existsSync(saveDir)) fs.mkdirSync(saveDir, { recursive: true })
482
482
 
483
483
  const fileName = `${Date.now()}_${Math.random().toString(36).substring(7)}.${ext}`
@@ -63,17 +63,20 @@ class WeixinPlugin extends Plugin {
63
63
  this._initialized = false
64
64
  // 预创建的 sessionScope 监听器(避免每次消息都创建/销毁)
65
65
  this._sessionScopes = new Map()
66
- const saveDir = path.join(process.cwd(), '.foliko', 'data', this.name)
67
- this.downloader=new FileDownloader({
68
- retries: 3,
69
- baseDir:saveDir
70
- })
66
+ this.downloader = null
71
67
  this.agent=null
72
68
  this.sessionId=null
73
69
  }
74
70
 
75
71
  install(framework) {
76
72
  this._framework = framework
73
+ if (!this.downloader) {
74
+ const saveDir = path.join(this._framework?.getCwd?.() ?? process.cwd(), '.foliko', 'data', this.name)
75
+ this.downloader = new FileDownloader({
76
+ retries: 3,
77
+ baseDir: saveDir
78
+ })
79
+ }
77
80
  // 注册微信发送工具
78
81
  this._registerTools()
79
82
  return this
@@ -473,7 +476,7 @@ class WeixinPlugin extends Plugin {
473
476
  name: `weixin_${userId}`,
474
477
  systemPrompt: this.systemPrompt,
475
478
  sharedPrompt: `工作目录: {{WORK_DIR}}`,
476
- metadata: { WORK_DIR: process.cwd() }
479
+ metadata: { WORK_DIR: this._framework?.getCwd?.() ?? process.cwd() }
477
480
  })
478
481
 
479
482
  this._sessionAgents.set(userId, agent)
@@ -247,7 +247,7 @@ class Skill {
247
247
  sessionId: framework?._currentSessionId || 'unknown',
248
248
  agent: null,
249
249
  framework: framework,
250
- cwd: process.cwd(), // 工作目录,用于路径解析
250
+ cwd: framework?.getCwd?.() ?? process.cwd(), // 工作目录,用于路径解析
251
251
  };
252
252
  // 支持 argumentParser 先解析参数
253
253
  let parsedArgs = toolArgs.command || '';
@@ -520,8 +520,9 @@ class SkillManagerPlugin extends Plugin {
520
520
  let totalLoaded = 0;
521
521
 
522
522
  // 1. 扫描 skillsDirs
523
+ const baseCwd = this._framework?.getCwd?.() ?? process.cwd();
523
524
  for (const skillsDir of this._skillsDirs) {
524
- const resolvedDir = path.resolve(process.cwd(), skillsDir);
525
+ const resolvedDir = path.resolve(baseCwd, skillsDir);
525
526
 
526
527
  if (!fs.existsSync(resolvedDir)) {
527
528
  continue;
@@ -881,7 +881,8 @@ class WorkflowPlugin extends Plugin {
881
881
  * 加载目录中的工作流定义
882
882
  */
883
883
  _loadWorkflows() {
884
- const dir = path.resolve(process.cwd(), this._workflowsDir);
884
+ const cwd = this._framework?.getCwd?.() ?? process.cwd();
885
+ const dir = path.resolve(cwd, this._workflowsDir);
885
886
  if (!fs.existsSync(dir)) {
886
887
  fs.mkdirSync(dir, { recursive: true });
887
888
  // log.info(` Created workflows directory: ${dir}`);
package/src/core/agent.js CHANGED
@@ -172,7 +172,7 @@ class Agent extends EventEmitter {
172
172
  switch (key) {
173
173
  case 'WORK_DIR':
174
174
  case 'CWD':
175
- return process.cwd();
175
+ return this.framework?.getCwd?.() ?? process.cwd();
176
176
  case 'HOME_DIR':
177
177
  return os.homedir();
178
178
  case 'HOST_NAME':
@@ -122,6 +122,9 @@ const SYSTEM_PLUGINS = new Set([
122
122
  /** Agent 配置目录名 */
123
123
  const AGENT_DIR = '.foliko';
124
124
 
125
+ /** 用户主目录下 Agent 配置目录名 */
126
+ const HOME_AGENT_DIR_NAME = '.foliko';
127
+
125
128
  // ============================================================================
126
129
  // Session 存储
127
130
  // ============================================================================
@@ -186,6 +189,7 @@ module.exports = {
186
189
  DEFAULT_PLUGIN_PRIORITY,
187
190
  SYSTEM_PLUGINS,
188
191
  AGENT_DIR,
192
+ HOME_AGENT_DIR_NAME,
189
193
 
190
194
  // Storage
191
195
  DEFAULT_SESSION_STORAGE_TYPE,
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  const path = require('path');
7
+ const os = require('os');
7
8
  const { AsyncLocalStorage } = require('async_hooks');
8
9
  const { EventEmitter } = require('../utils/event-emitter');
9
10
  const { PluginManager } = require('./plugin-manager');
@@ -15,6 +16,7 @@ const { ContextManager } = require('./context-manager');
15
16
  const { SessionManager } = require('./session-manager');
16
17
  const { CoordinatorManager } = require('./coordinator-manager');
17
18
  const { Logger, LOG_LEVELS } = require('../utils/logger');
19
+ const { AGENT_DIR, HOME_AGENT_DIR_NAME, SYSTEM_PLUGINS } = require('./constants');
18
20
  // 创建一个连接到 Framework 的 logger
19
21
  function createFrameworkLogger(framework) {
20
22
  const logger = new Logger({ namespace: 'foliko' });
@@ -92,6 +94,10 @@ class Framework extends EventEmitter {
92
94
  /**
93
95
  * @param {Object} config - 配置
94
96
  * @param {boolean} [config.debug=false] - 调试模式
97
+ * @param {boolean} [config.silent=false] - 是否静默(不输出日志)
98
+ * @param {string} [config.cwd] - 初始工作目录(默认 process.cwd(),可后续用 setCwd 切换)
99
+ * @param {string} [config.agentDir='.foliko'] - 项目级 Agent 目录名
100
+ * @param {string} [config.homeAgentDir] - 用户级 Agent 目录(默认 ~/.foliko,可被 FOLIKO_HOME 环境变量覆盖)
95
101
  */
96
102
  constructor(config = {}) {
97
103
  super();
@@ -130,6 +136,15 @@ class Framework extends EventEmitter {
130
136
  baseDir: '.foliko/sessions',
131
137
  };
132
138
 
139
+ // 工作目录与 Agent 目录
140
+ this._cwd = config.cwd || process.cwd();
141
+ this._agentDir = config.agentDir || AGENT_DIR;
142
+ this._homeAgentDir =
143
+ config.homeAgentDir ||
144
+ process.env.FOLIKO_HOME ||
145
+ path.join(os.homedir(), HOME_AGENT_DIR_NAME);
146
+ this._changingCwd = false;
147
+
133
148
  // 注册 logger 到 framework
134
149
  this.logger = createFrameworkLogger(this);
135
150
 
@@ -525,7 +540,7 @@ class Framework extends EventEmitter {
525
540
  }
526
541
 
527
542
  // 获取工作目录
528
- const cwd = options.cwd || process.cwd();
543
+ const cwd = options.cwd || this._cwd;
529
544
  const sessionDir = path.join(cwd, '.foliko', 'sessions');
530
545
 
531
546
  // 获取或创建 session file path
@@ -567,7 +582,7 @@ class Framework extends EventEmitter {
567
582
  return manager;
568
583
  }
569
584
 
570
- const cwd = options.cwd || process.cwd();
585
+ const cwd = options.cwd || this._cwd;
571
586
  const sessionDir = path.join(cwd, '.foliko', 'sessions');
572
587
  const sessionFile = path.join(sessionDir, `${sessionId}.jsonl`);
573
588
 
@@ -673,6 +688,230 @@ class Framework extends EventEmitter {
673
688
  return this;
674
689
  }
675
690
 
691
+ // ============================================================================
692
+ // 工作目录与 Agent 目录
693
+ // ============================================================================
694
+
695
+ /**
696
+ * 获取当前工作目录
697
+ * @returns {string}
698
+ */
699
+ getCwd() {
700
+ return this._cwd;
701
+ }
702
+
703
+ /**
704
+ * 获取项目级 Agent 目录(绝对路径)
705
+ * @returns {string}
706
+ */
707
+ getAgentDir() {
708
+ return path.resolve(this._cwd, this._agentDir);
709
+ }
710
+
711
+ /**
712
+ * 获取用户级 Agent 目录(绝对路径)
713
+ * @returns {string}
714
+ */
715
+ getHomeAgentDir() {
716
+ return this._homeAgentDir;
717
+ }
718
+
719
+ /**
720
+ * 解析项目级 Agent 目录下的相对路径
721
+ * @param {...string} parts
722
+ * @returns {string}
723
+ */
724
+ resolveAgentPath(...parts) {
725
+ return path.join(this.getAgentDir(), ...parts);
726
+ }
727
+
728
+ /**
729
+ * 解析用户级 Agent 目录下的相对路径
730
+ * @param {...string} parts
731
+ * @returns {string}
732
+ */
733
+ resolveHomeAgentPath(...parts) {
734
+ return path.join(this.getHomeAgentDir(), ...parts);
735
+ }
736
+
737
+ /**
738
+ * 动态切换工作目录
739
+ *
740
+ * 切换后会自动:
741
+ * 1. 更新 PluginManager 状态文件位置
742
+ * 2. 销毁已存在的 Session 上下文(旧路径失效)
743
+ * 3. 通知所有已加载插件(onCwdChanged 钩子)并触发 reload
744
+ * 4. 发送 cwd:changed 事件
745
+ *
746
+ * @param {string} newCwd - 新工作目录(相对或绝对路径)
747
+ * @param {Object} [options]
748
+ * @param {boolean} [options.reload=true] - 是否自动重载已加载插件
749
+ * @param {string} [options.agentDir] - 顺带覆盖项目 agent 目录名
750
+ * @param {string} [options.homeAgentDir] - 顺带覆盖用户 agent 目录路径
751
+ * @returns {Promise<this>}
752
+ */
753
+ async setCwd(newCwd, options = {}) {
754
+ if (this._changingCwd) {
755
+ throw new Error('setCwd re-entered during cwd change');
756
+ }
757
+
758
+ const oldCwd = this._cwd;
759
+ const resolved = path.resolve(newCwd);
760
+ if (resolved === oldCwd) {
761
+ return this;
762
+ }
763
+
764
+ this._changingCwd = true;
765
+ try {
766
+ this._cwd = resolved;
767
+ if (options.agentDir) this._agentDir = options.agentDir;
768
+ if (options.homeAgentDir) this._homeAgentDir = options.homeAgentDir;
769
+
770
+ // 1. 更新 PluginManager 状态文件位置
771
+ this.pluginManager._setStateFile(resolved);
772
+
773
+ // 2. 销毁已存在的 Session 上下文(旧路径失效)
774
+ const count = this._sessionContexts.size;
775
+ for (const sid of Array.from(this._sessionContexts.keys())) {
776
+ this.destroySessionContext(sid);
777
+ }
778
+ this.emit('session:contexts-invalidated', { oldCwd, newCwd: resolved, count });
779
+
780
+ // 3. 通知已加载插件 + 触发 reload
781
+ if (options.reload !== false) {
782
+ for (const entry of this.pluginManager.getAll()) {
783
+ const inst = entry.instance;
784
+ try {
785
+ if (typeof inst?.onCwdChanged === 'function') {
786
+ await inst.onCwdChanged(oldCwd, resolved, this);
787
+ }
788
+ await this.pluginManager.reload(entry.name);
789
+ } catch (err) {
790
+ this.logger.warn(`Plugin ${entry.name} failed during cwd change: ${err.message}`);
791
+ }
792
+ }
793
+ }
794
+
795
+ // 4. 发送 cwd:changed 事件
796
+ this.emit('cwd:changed', { oldCwd, newCwd: resolved, framework: this });
797
+ } finally {
798
+ this._changingCwd = false;
799
+ }
800
+ return this;
801
+ }
802
+
803
+ // ============================================================================
804
+ // 项目重扫 — 重新扫描当前 cwd 下的插件与技能
805
+ // ============================================================================
806
+
807
+ /**
808
+ * 重新扫描当前 cwd 下的插件与技能目录
809
+ *
810
+ * 工作流程:
811
+ * 1. 卸载 keepLoaded 列表外的所有已加载插件
812
+ * 2. 重新读取 DefaultPlugins 配置(新的 skillsDirs / agentsDir / pluginLinks)
813
+ * 3. 重新跑 bootstrapDefaults 加载所有默认插件(核心 + 自定义)
814
+ * 4. 同步 SkillManager._skillsDirs 并触发技能重扫
815
+ * 5. 发出 rescan:complete 事件
816
+ *
817
+ * 典型用法: 配合 setCwd 切换到新项目
818
+ * await framework.setCwd('D:/new-project');
819
+ * await framework.rescanProject();
820
+ *
821
+ * @param {Object} [options]
822
+ * @param {string[]} [options.keepLoaded] - 保留的插件名列表(默认保留系统插件)
823
+ * @param {boolean} [options.reloadKept=false] - 是否对 keepLoaded 中的插件也执行 reload
824
+ * @param {boolean} [options.reloadSkillManager=true] - 是否重扫技能
825
+ * @returns {Promise<{unloaded: string[], loaded: string[], keepLoaded: string[]}>}
826
+ */
827
+ async rescanProject(options = {}) {
828
+ const { bootstrapDefaults, DefaultPlugins, loadAgentConfig } = require('../../plugins/default-plugins');
829
+ const keepList = options.keepLoaded || Array.from(SYSTEM_PLUGINS);
830
+ const keepSet = new Set(keepList);
831
+ const unloadList = [];
832
+ const loadList = [];
833
+
834
+ // 1. 卸载 keepSet 之外的插件
835
+ for (const entry of Array.from(this.pluginManager.getAll())) {
836
+ if (keepSet.has(entry.name)) continue;
837
+ const name = entry.name;
838
+ try {
839
+ await this.pluginManager.unload(name);
840
+ // 同步从注册表移除,避免 bootstrapDefaults 的 has() 跳过
841
+ this.pluginManager._plugins.delete(name);
842
+ unloadList.push(name);
843
+ } catch (err) {
844
+ this.logger.warn(`rescanProject: failed to unload ${name}: ${err.message}`);
845
+ }
846
+ }
847
+
848
+ // 2. 确保 defaults 插件已加载,重新读取配置
849
+ let defaultsPlugin = this.pluginManager.get('defaults');
850
+ if (!defaultsPlugin) {
851
+ const dp = new DefaultPlugins({ agentDir: this._agentDir });
852
+ try {
853
+ await this.loadPlugin(dp);
854
+ defaultsPlugin = dp;
855
+ } catch (err) {
856
+ this.logger.warn(`rescanProject: failed to load defaults: ${err.message}`);
857
+ }
858
+ } else {
859
+ try {
860
+ await defaultsPlugin.reload(this);
861
+ } catch (err) {
862
+ this.logger.warn(`rescanProject: failed to reload defaults: ${err.message}`);
863
+ }
864
+ }
865
+ const agentConfig = defaultsPlugin
866
+ ? defaultsPlugin.getConfig() || loadAgentConfig(this, this._agentDir)
867
+ : loadAgentConfig(this, this._agentDir);
868
+
869
+ // 3. 跑 bootstrapDefaults 加载默认插件(核心 + 自定义)
870
+ const beforeNames = new Set(this.pluginManager.getAll().map((p) => p.name));
871
+ try {
872
+ await bootstrapDefaults(this, { _config: agentConfig, _skipConfigLoad: true });
873
+ } catch (err) {
874
+ this.logger.warn(`rescanProject: bootstrapDefaults failed: ${err.message}`);
875
+ }
876
+ for (const name of this.pluginManager.getAll().map((p) => p.name)) {
877
+ if (!beforeNames.has(name)) loadList.push(name);
878
+ }
879
+
880
+ // 4. 同步并重扫技能
881
+ if (options.reloadSkillManager !== false) {
882
+ const skillManager = this.pluginManager.get('skill-manager');
883
+ if (skillManager) {
884
+ if (Array.isArray(agentConfig.skillsDirs)) {
885
+ skillManager._skillsDirs = agentConfig.skillsDirs;
886
+ }
887
+ try {
888
+ await skillManager.reload(this);
889
+ } catch (err) {
890
+ this.logger.warn(`rescanProject: skill-manager reload failed: ${err.message}`);
891
+ }
892
+ }
893
+ }
894
+
895
+ // 5. (可选) 对保留的插件也执行 reload
896
+ if (options.reloadKept) {
897
+ for (const name of keepList) {
898
+ if (!this.pluginManager.has(name)) continue;
899
+ try {
900
+ await this.pluginManager.reload(name);
901
+ } catch (err) {
902
+ this.logger.warn(`rescanProject: reload kept ${name} failed: ${err.message}`);
903
+ }
904
+ }
905
+ }
906
+
907
+ const result = { unloaded: unloadList, loaded: loadList, keepLoaded: keepList };
908
+ this.emit('rescan:complete', result);
909
+ this.logger.info(
910
+ `rescanProject: unloaded=${unloadList.length} loaded=${loadList.length} kept=${keepList.length}`
911
+ );
912
+ return result;
913
+ }
914
+
676
915
  /**
677
916
  * 启动 Session 自动清理
678
917
  * @param {number} [intervalMs=60000] - 检查间隔
@@ -244,6 +244,17 @@ class Plugin {
244
244
  this._autoRegisterSubAgents();
245
245
  }
246
246
 
247
+ /**
248
+ * 工作目录变更钩子 — 框架级 cwd 切换时由 Framework.setCwd 调用
249
+ * 子类可重写以重新加载 cwd 相关的资源(路径、文件句柄等)
250
+ * @param {string} oldCwd
251
+ * @param {string} newCwd
252
+ * @param {Framework} framework
253
+ */
254
+ onCwdChanged(oldCwd, newCwd, framework) {
255
+ // 默认 no-op
256
+ }
257
+
247
258
  /**
248
259
  * 卸载插件 - 清理资源
249
260
  * @param {Framework} framework - 框架实例
@@ -22,7 +22,12 @@ class PluginManager {
22
22
  this.framework = framework;
23
23
  this._plugins = new Map();
24
24
  this._loading = false;
25
- this._stateFile = path.join(process.cwd(), '.foliko', 'data', 'plugins-state.json');
25
+ this._stateFile = path.join(
26
+ framework?.getCwd?.() ?? process.cwd(),
27
+ '.foliko',
28
+ 'data',
29
+ 'plugins-state.json'
30
+ );
26
31
  this._knownPlugins = new Set(); // 已知插件列表(包括未加载的)
27
32
 
28
33
  // 创建子日志器
@@ -111,6 +116,16 @@ class PluginManager {
111
116
  return this._stateFile;
112
117
  }
113
118
 
119
+ /**
120
+ * 更新状态文件路径(Framework.setCwd 时调用)
121
+ * @param {string} cwd - 新的工作目录
122
+ * @private
123
+ */
124
+ _setStateFile(cwd) {
125
+ this._stateFile = path.join(cwd, '.foliko', 'data', 'plugins-state.json');
126
+ this._stateCache = null;
127
+ }
128
+
114
129
  /**
115
130
  * 保存插件状态到文件
116
131
  * 注意:AI 插件配置不保存(从环境变量和命令行获取)
@@ -442,7 +457,8 @@ class PluginManager {
442
457
  * @private
443
458
  */
444
459
  async _discoverCustomPlugins() {
445
- const pluginsDir = path.resolve(process.cwd(), '.foliko', 'plugins');
460
+ const cwd = this.framework?.getCwd?.() ?? process.cwd();
461
+ const pluginsDir = path.resolve(cwd, '.foliko', 'plugins');
446
462
  if (!fs.existsSync(pluginsDir)) {
447
463
  return;
448
464
  }