@jack200714/mafw 4.8.0 → 4.10.2

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 (42) hide show
  1. package/README.md +27 -3
  2. package/gateway/dist/core/manager/goal-snapshot.js +2 -2
  3. package/gateway/dist/core/manager/manager-session-runtime.js +59 -0
  4. package/gateway/dist/core/manager/milestone-push.js +25 -10
  5. package/gateway/dist/index.js +465 -440
  6. package/gateway/dist/media/media-plugin-loader.js +25 -1
  7. package/gateway/dist/media/resolve-prompt.js +20 -0
  8. package/gateway/dist/memory/gateway-db.js +23 -0
  9. package/gateway/dist/opencode-adapter.js +53 -4
  10. package/gateway/dist/plugins/package-context.js +24 -0
  11. package/gateway/dist/plugins/package-host.js +331 -0
  12. package/gateway/dist/plugins/package-types.js +2 -0
  13. package/gateway/dist/recall/gateway-db-migrate.js +5 -2
  14. package/gateway/dist/recall/redact.js +53 -0
  15. package/gateway/dist/recall/turn-pipeline.js +2 -0
  16. package/gateway/dist/routes/event-publish.js +44 -0
  17. package/gateway/dist/routes/plugins.js +4 -1
  18. package/gateway/dist/routes/registry.js +125 -0
  19. package/gateway/dist/routes/route-catalog.js +192 -0
  20. package/gateway/dist/routes/triage-dismiss.js +32 -0
  21. package/gateway/dist/routes/waitwhat-command.js +43 -0
  22. package/gateway/dist/routes/wave1-handlers.js +62 -0
  23. package/gateway/dist/routes/wave2-handlers.js +47 -0
  24. package/gateway/dist/runtime/contract.js +4 -1
  25. package/gateway/dist/runtime/event-broadcast.js +8 -0
  26. package/gateway/dist/runtime/loader.js +22 -1
  27. package/gateway/dist/runtime/normalize.js +13 -0
  28. package/gateway/dist/runtime/opencode-runtime.js +18 -2
  29. package/gateway/dist/runtime/pi/pi-approval-bridge.js +12 -2
  30. package/gateway/dist/runtime/pi/pi-approval-extension.js +11 -3
  31. package/gateway/dist/runtime/pi/pi-session.js +21 -3
  32. package/gateway/dist/runtime/plugins/pi-runtime.js +6 -3
  33. package/gateway/dist/runtime/serve-sidecar.js +4 -1
  34. package/gateway/dist/runtime/serve-supervisor.js +19 -20
  35. package/gateway/dist/runtime/validate.js +39 -0
  36. package/gateway/dist/skills/manager-identity.js +6 -1
  37. package/gateway/dist/usage/builtin-plugins/gateway.js +91 -36
  38. package/gateway/dist/usage/plugin-context.js +42 -2
  39. package/gateway/dist/usage/plugin-loader.js +32 -2
  40. package/gateway/package.json +2 -1
  41. package/package.json +3 -1
  42. package/packages/tui/dist/cli.js +114 -51
@@ -44,6 +44,8 @@ class MediaPluginLoader {
44
44
  pluginsDir;
45
45
  state = new Map();
46
46
  engines = new Map();
47
+ builtinEngines = new Map();
48
+ packageEngines = new Map();
47
49
  watcher;
48
50
  debounceTimer;
49
51
  getCredentials;
@@ -146,8 +148,30 @@ class MediaPluginLoader {
146
148
  logger_1.log.warn(`[MediaPluginLoader] ${file} load error: ${err.message}`);
147
149
  }
148
150
  }
151
+ /** 内置引擎登记(如 pi)。prompt 是占位——真正的内置路径由 resolveMediaPrompt 在调用点解析。 */
152
+ registerBuiltinEngine(name, modalities) {
153
+ this.builtinEngines.set(name, {
154
+ prompt: (async () => { throw new Error('builtin engine prompt is resolved at call site'); }),
155
+ modalities,
156
+ builtin: true,
157
+ });
158
+ }
159
+ getBuiltinEngineNames() {
160
+ return [...this.builtinEngines.keys()];
161
+ }
162
+ /** PluginHost 推送的包贡献。同名覆盖 legacy 与内置。 */
163
+ setPackageEntries(entries) {
164
+ this.packageEngines = new Map(entries.map((e) => [e.name, e]));
165
+ }
149
166
  getEngines() {
150
- return this.engines;
167
+ // 合并顺序:内置 → legacy 文件 → 包(后者覆盖前者同名)
168
+ const merged = new Map(this.builtinEngines);
169
+ for (const [name, e] of this.engines)
170
+ merged.set(name, e);
171
+ for (const [name, e] of this.packageEngines) {
172
+ merged.set(name, { prompt: e.prompt, modalities: e.modalities, source: e.source });
173
+ }
174
+ return merged;
151
175
  }
152
176
  getState() {
153
177
  return [...this.state.values()];
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveMediaPrompt = resolveMediaPrompt;
4
+ /** media 引擎解析(deps 注入可单测)。用户引擎可同名覆盖内置 pi。 */
5
+ const logger_1 = require("../core/utils/logger");
6
+ function resolveMediaPrompt(engineName, kind, deps) {
7
+ const engine = deps.engines.get(engineName);
8
+ if (!engine) {
9
+ if (engineName !== 'pi')
10
+ logger_1.log.warn(`[MediaService] engine '${engineName}' not found, falling back to pi`);
11
+ return deps.builtinPi();
12
+ }
13
+ if (engine.builtin)
14
+ return deps.builtinPi();
15
+ if (!engine.modalities.includes(kind)) {
16
+ logger_1.log.warn(`[MediaService] engine '${engineName}' does not support modality '${kind}', falling back to pi`);
17
+ return deps.builtinPi();
18
+ }
19
+ return engine.prompt;
20
+ }
@@ -371,6 +371,12 @@ class GatewayDatabase {
371
371
  return this.db.prepare('SELECT COUNT(*) AS c FROM t1_observations').get().c;
372
372
  }
373
373
  // ── KV store (small critical state) ─────────────────────────────────────
374
+ // Scope 归属规范(新增 scope 必须声明一类):
375
+ // - runtime-scoped:生命周期绑定当前 agent runtime(会话 id 属于 runtime 存储),
376
+ // 切换时由 Scheduler.invalidateRuntimeScopedKv 统一失效——
377
+ // manager-session / internal-session / reflect-cursor
378
+ // - durable:跨 runtime 有效——registry/snapshot / milestone-notified
379
+ // runtime-scoped 条目的 value 必须携带 `at`(ISO 日期)供 TTL/审计。
374
380
  kvGet(scope, key) {
375
381
  const row = this.db.prepare('SELECT value FROM kv_store WHERE scope = ? AND key = ?').get(scope, key);
376
382
  if (!row)
@@ -402,6 +408,23 @@ class GatewayDatabase {
402
408
  }
403
409
  });
404
410
  }
411
+ kvClearScope(scope) {
412
+ const info = this.db.prepare('DELETE FROM kv_store WHERE scope = ?').run(scope);
413
+ return info.changes;
414
+ }
415
+ kvPruneOlderThan(scope, ttlDays) {
416
+ const now = Date.now();
417
+ let removed = 0;
418
+ for (const { key, value } of this.kvAll(scope)) {
419
+ const at = value && typeof value === 'object' ? value.at : undefined;
420
+ const ts = typeof at === 'string' ? Date.parse(at) : NaN;
421
+ if (!isNaN(ts) && now - ts > ttlDays * 86_400_000) {
422
+ this.kvDelete(scope, key);
423
+ removed++;
424
+ }
425
+ }
426
+ return removed;
427
+ }
405
428
  // ── Goal outcomes (RSI Phase 1) ─────────────────────────────────────────
406
429
  upsertGoalOutcome(o) {
407
430
  this.db.prepare(`
@@ -26,7 +26,43 @@ function messageToParts(message, parts) {
26
26
  // ─── 适配层实现 ─────────────────────────────────────────────
27
27
  async function createOpencodeAdapter(config) {
28
28
  const { createOpencodeClient } = await import('@opencode-ai/sdk/v2');
29
- const client = createOpencodeClient(config);
29
+ let current = createOpencodeClient(config);
30
+ let clientBaseUrl = config.baseUrl;
31
+ // URL 吸收:runtime 的 spawnServe 会把 sidecar 实际地址写回 config.baseUrl
32
+ // (动态端口前提)。SDK 客户端内部持有构造期 config 快照,感知不到变更——
33
+ // 每次访问按 baseUrl 惰性重建(幂等,无变更零开销)。directNative/
34
+ // healthCheck 直读 config.baseUrl,天然跟随。
35
+ const client = new Proxy(current, {
36
+ get(_t, prop) {
37
+ if (config.baseUrl !== clientBaseUrl) {
38
+ clientBaseUrl = config.baseUrl;
39
+ current = createOpencodeClient({ ...config, baseUrl: config.baseUrl });
40
+ }
41
+ return Reflect.get(current, prop, current);
42
+ },
43
+ });
44
+ // opencode 原生 V1 路由的 workspace 路由细节收敛在 adapter 内(契约保持
45
+ // runtime 中立)——带 directory 的调用直连 fetch(V2 SDK 调用不带 workspace 语义)。
46
+ const directNative = async (path, method, body, directory) => {
47
+ const url = `${config.baseUrl}${path}${directory ? `?directory=${encodeURIComponent(directory)}` : ''}`;
48
+ const res = await fetch(url, {
49
+ method,
50
+ headers: {
51
+ ...(body !== undefined ? { 'content-type': 'application/json' } : {}),
52
+ ...(directory ? { 'x-opencode-directory': encodeURIComponent(directory) } : {}),
53
+ ...(config.headers ?? {}),
54
+ },
55
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
56
+ });
57
+ if (!res.ok)
58
+ throw new Error(`opencode native ${path} failed: ${res.status}`);
59
+ try {
60
+ return await res.json();
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ };
30
66
  return {
31
67
  session: {
32
68
  async create(opts) {
@@ -187,12 +223,17 @@ async function createOpencodeAdapter(config) {
187
223
  },
188
224
  question: {
189
225
  async list(opts) {
190
- const result = await client.session.question.list(opts?.directory ? { directory: opts.directory } : undefined);
226
+ // SDK 1.18.x:question OpencodeClient 顶层命名空间(session 上没有)
227
+ const result = await client.question.list(opts?.directory ? { directory: opts.directory } : undefined);
191
228
  const data = unwrap(result);
192
229
  return Array.isArray(data) ? data : data?.items ?? [];
193
230
  },
194
231
  async reply(opts) {
195
- const result = await client.session.question.reply({
232
+ if (opts.directory) {
233
+ await directNative(`/question/${opts.requestID}/reply`, 'POST', { answers: opts.answers }, opts.directory);
234
+ return;
235
+ }
236
+ const result = await client.question.reply({
196
237
  requestID: opts.requestID,
197
238
  answers: opts.answers,
198
239
  });
@@ -201,12 +242,20 @@ async function createOpencodeAdapter(config) {
201
242
  }
202
243
  },
203
244
  async reject(opts) {
204
- const result = await client.session.question.reject({ requestID: opts.requestID });
245
+ if (opts.directory) {
246
+ await directNative(`/question/${opts.requestID}/reject`, 'POST', undefined, opts.directory);
247
+ return;
248
+ }
249
+ const result = await client.question.reject({ requestID: opts.requestID });
205
250
  if (result && typeof result === 'object' && 'error' in result && result.error) {
206
251
  throw new Error(String(result.error));
207
252
  }
208
253
  },
209
254
  },
255
+ async permissionList(opts) {
256
+ const data = await directNative('/permission', 'GET', undefined, opts?.directory);
257
+ return Array.isArray(data) ? data : data?.items ?? [];
258
+ },
210
259
  },
211
260
  global: {
212
261
  event() {
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createPluginPackageContext = createPluginPackageContext;
4
+ const config_1 = require("../config");
5
+ const logger_1 = require("../core/utils/logger");
6
+ const auth_1 = require("../runtime/auth");
7
+ const EMPTY_USAGE = { modelStats: () => [] };
8
+ function createPluginPackageContext(name, deps) {
9
+ return {
10
+ log: logger_1.log,
11
+ fetch: (url, opts) => fetch(url, { ...opts, signal: opts?.signal ?? AbortSignal.timeout(60_000) }),
12
+ apiKey: (provider) => (0, auth_1.getProviderApiKey)(provider, undefined, deps.getCredentials?.()) ?? null,
13
+ pluginConfig: () => config_1.config.raw?.plugins?.[name]?.config
14
+ ?? config_1.config.raw?.usage?.pluginConfig?.[name]
15
+ ?? config_1.config.raw?.media?.pluginConfig?.[name]
16
+ ?? config_1.config.raw?.runtime?.pluginConfig?.[name]
17
+ ?? {},
18
+ credentials: deps.getCredentials?.(),
19
+ projectDir: deps.projectDir,
20
+ gatewayPort: deps.gatewayPort,
21
+ usage: deps.usageStats?.() ?? EMPTY_USAGE,
22
+ emit: (event) => deps.emit?.(event),
23
+ };
24
+ }
@@ -0,0 +1,331 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PluginHost = void 0;
37
+ /**
38
+ * 统一插件包宿主 —— 扫 ~/.mafw/plugins/(单文件包 *.js / 目录包 plugin.json+入口),
39
+ * 激活后把贡献分发给三个 legacy loader(setPackageEntries)。fail-open。
40
+ * 优先级(同名):包 > legacy 目录文件 > 内置。
41
+ */
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const logger_1 = require("../core/utils/logger");
45
+ const pi_adapter_1 = require("../media/pi-adapter");
46
+ const VALID_MODALITIES = new Set(['image', 'video', 'audio']);
47
+ class PluginHost {
48
+ dir;
49
+ createContext;
50
+ state = new Map();
51
+ watcher;
52
+ dirWatchers = new Map();
53
+ debounce;
54
+ /** 当前目录包的子目录绝对路径(refreshDirWatchers 用) */
55
+ pkgDirs = [];
56
+ runtimeCb;
57
+ mediaCb;
58
+ usageCb;
59
+ lastRuntime = [];
60
+ lastMedia = [];
61
+ lastUsage = [];
62
+ constructor(dir, createContext) {
63
+ this.dir = dir;
64
+ this.createContext = createContext;
65
+ }
66
+ bindRuntime(cb) { this.runtimeCb = cb; cb(this.lastRuntime); }
67
+ bindMedia(cb) { this.mediaCb = cb; cb(this.lastMedia); }
68
+ bindUsage(cb) { this.usageCb = cb; cb(this.lastUsage); }
69
+ async init() {
70
+ this.ensureDir();
71
+ await this.reload();
72
+ this.startWatch();
73
+ }
74
+ getState() {
75
+ return [...this.state.values()].sort((a, b) => a.name.localeCompare(b.name));
76
+ }
77
+ stop() {
78
+ this.watcher?.close();
79
+ for (const w of this.dirWatchers.values())
80
+ w.close();
81
+ this.dirWatchers.clear();
82
+ if (this.debounce)
83
+ clearTimeout(this.debounce);
84
+ }
85
+ ensureDir() {
86
+ if (!fs.existsSync(this.dir)) {
87
+ fs.mkdirSync(this.dir, { recursive: true });
88
+ fs.writeFileSync(path.join(this.dir, 'README.md'), README_CONTENT);
89
+ logger_1.log.info(`[PluginHost] Created ${this.dir}`);
90
+ }
91
+ }
92
+ /** 全量重扫 + 重新激活 + 一次性推送(跨面原子:先全部激活完再推各 loader)。 */
93
+ async reload() {
94
+ const next = new Map();
95
+ const runtime = [];
96
+ const media = [];
97
+ const usage = [];
98
+ const pkgDirs = [];
99
+ for (const desc of this.scanDescriptors(next)) {
100
+ if (desc.pkgDir)
101
+ pkgDirs.push(desc.pkgDir);
102
+ try {
103
+ const c = await this.activate(desc);
104
+ if (c.runtime)
105
+ runtime.push(c.runtime);
106
+ if (c.media)
107
+ media.push(c.media);
108
+ if (c.usage)
109
+ usage.push(c.usage);
110
+ next.set(desc.name, {
111
+ name: desc.name, source: desc.mainFile, status: 'ok',
112
+ contributions: c.contributions, version: desc.manifest?.version,
113
+ });
114
+ }
115
+ catch (err) {
116
+ next.set(desc.name, { name: desc.name, source: desc.mainFile, status: 'error', error: err.message, contributions: [] });
117
+ logger_1.log.warn(`[PluginHost] ${desc.name} activate error: ${err.message}`);
118
+ }
119
+ }
120
+ this.state = next;
121
+ this.pkgDirs = pkgDirs;
122
+ this.lastRuntime = runtime;
123
+ this.lastMedia = media;
124
+ this.lastUsage = usage;
125
+ this.runtimeCb?.(runtime);
126
+ this.mediaCb?.(media);
127
+ this.usageCb?.(usage);
128
+ this.refreshDirWatchers();
129
+ }
130
+ /** 每个目录包一个 watcher(跨平台——不用 recursive)。reload 后按当前包集合增删。 */
131
+ refreshDirWatchers() {
132
+ if (!this.watcher)
133
+ return; // 顶层 watch 未建立(init 前手动 reload)则不建
134
+ const wanted = new Set(this.pkgDirs);
135
+ for (const [dir, w] of this.dirWatchers) {
136
+ if (!wanted.has(dir)) {
137
+ w.close();
138
+ this.dirWatchers.delete(dir);
139
+ }
140
+ }
141
+ for (const dir of wanted) {
142
+ if (this.dirWatchers.has(dir))
143
+ continue;
144
+ try {
145
+ const w = fs.watch(dir, () => {
146
+ if (this.debounce)
147
+ clearTimeout(this.debounce);
148
+ this.debounce = setTimeout(() => {
149
+ void this.reload().catch((err) => logger_1.log.warn(`[PluginHost] reload error: ${err.message}`));
150
+ }, 300);
151
+ });
152
+ this.dirWatchers.set(dir, w);
153
+ }
154
+ catch { /* 单个目录不可 watch → 跳过,fail-open */ }
155
+ }
156
+ }
157
+ /** 枚举候选包;逐条错误记入 errorStates(不抛出)。 */
158
+ scanDescriptors(errorStates) {
159
+ const out = [];
160
+ if (!fs.existsSync(this.dir))
161
+ return out;
162
+ const entries = fs.readdirSync(this.dir, { withFileTypes: true })
163
+ .sort((a, b) => a.name.localeCompare(b.name));
164
+ for (const entry of entries) {
165
+ try {
166
+ if (entry.isFile() && entry.name.endsWith('.js')) {
167
+ out.push({ name: entry.name.replace(/\.js$/, ''), mainFile: path.join(this.dir, entry.name) });
168
+ }
169
+ else if (entry.isDirectory()) {
170
+ const pkgDir = path.join(this.dir, entry.name);
171
+ const manifestPath = path.join(pkgDir, 'plugin.json');
172
+ let manifest;
173
+ if (fs.existsSync(manifestPath)) {
174
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
175
+ }
176
+ const mainFile = path.join(pkgDir, manifest?.main ?? 'index.js');
177
+ if (!fs.existsSync(mainFile))
178
+ throw new Error(`main file not found: ${manifest?.main ?? 'index.js'}`);
179
+ out.push({ name: manifest?.name ?? entry.name, mainFile, manifest, pkgDir });
180
+ }
181
+ }
182
+ catch (err) {
183
+ errorStates.set(entry.name, { name: entry.name, source: path.join(this.dir, entry.name), status: 'error', error: err.message, contributions: [] });
184
+ }
185
+ }
186
+ return out;
187
+ }
188
+ async activate(desc) {
189
+ try {
190
+ delete require.cache[require.resolve(desc.mainFile)];
191
+ }
192
+ catch { /* first load */ }
193
+ const mod = require(desc.mainFile);
194
+ const modName = mod?.name;
195
+ if (typeof modName !== 'string' || !modName)
196
+ throw new Error('missing name');
197
+ if (modName !== desc.name)
198
+ throw new Error(`name mismatch: exports '${modName}', package '${desc.name}'`);
199
+ const ctx = this.createContext(modName);
200
+ const contribs = typeof mod.activate === 'function' ? ((await mod.activate(ctx)) ?? {}) : pickContributions(mod);
201
+ const kinds = [];
202
+ let runtime;
203
+ let media;
204
+ let usage;
205
+ if (contribs.usage) {
206
+ const u = contribs.usage;
207
+ if (typeof u.name !== 'string' || !u.name)
208
+ throw new Error('usage contribution missing name');
209
+ if (typeof u.fetch !== 'function')
210
+ throw new Error('usage contribution missing fetch(ctx)');
211
+ usage = { mod: u, source: desc.mainFile };
212
+ kinds.push('usage');
213
+ }
214
+ if (contribs.media) {
215
+ const spec = contribs.media;
216
+ if (!Array.isArray(spec.modalities) || spec.modalities.length === 0
217
+ || !spec.modalities.every((m) => VALID_MODALITIES.has(m)))
218
+ throw new Error('media: invalid modalities');
219
+ const hasCreatePrompt = typeof spec.createPrompt === 'function';
220
+ const hasPiEngine = spec.engine === 'pi';
221
+ if (hasCreatePrompt === hasPiEngine)
222
+ throw new Error('media: createPrompt and engine:"pi" are mutually exclusive');
223
+ const prompt = hasCreatePrompt
224
+ ? await spec.createPrompt(ctx)
225
+ : (0, pi_adapter_1.createPiPromptAdapter)({ fixPayload: spec.fixPayload, credentials: ctx.credentials });
226
+ if (typeof prompt !== 'function')
227
+ throw new Error('media: createPrompt did not return a function');
228
+ media = { name: spec.name ?? modName, prompt, modalities: spec.modalities, source: desc.mainFile };
229
+ kinds.push('media');
230
+ }
231
+ if (contribs.runtime) {
232
+ const spec = contribs.runtime;
233
+ if (typeof spec.createRuntime !== 'function')
234
+ throw new Error('runtime: missing createRuntime(ctx)');
235
+ runtime = {
236
+ name: spec.name ?? modName,
237
+ // 包 runtime 一律拿统一 ctx(忽略 loader 传入的 legacy ctx)
238
+ createRuntime: (_ignored) => spec.createRuntime(ctx),
239
+ capabilities: spec.capabilities ?? {},
240
+ external: spec.external !== false,
241
+ source: desc.mainFile,
242
+ };
243
+ kinds.push('runtime');
244
+ }
245
+ if (contribs.uiTools) {
246
+ // 声明期字段级校验:桌面 validateCard 是运行时兜底,形状错位应在激活期暴露
247
+ if (typeof contribs.uiTools !== 'object' || Array.isArray(contribs.uiTools)) {
248
+ throw new Error(`uiTools must be an object (got ${typeof contribs.uiTools})`);
249
+ }
250
+ for (const [tool, def] of Object.entries(contribs.uiTools)) {
251
+ if (!def || typeof def !== 'object' || Array.isArray(def)) {
252
+ throw new Error(`uiTools.${tool} must be an object (got ${Array.isArray(def) ? 'array' : typeof def})`);
253
+ }
254
+ }
255
+ kinds.push('uiTools');
256
+ }
257
+ if (kinds.length === 0)
258
+ throw new Error('no contributions (usage/media/runtime/uiTools)');
259
+ return { contributions: kinds, runtime, media, usage };
260
+ }
261
+ startWatch() {
262
+ if (!fs.existsSync(this.dir))
263
+ return;
264
+ try {
265
+ this.watcher = fs.watch(this.dir, () => {
266
+ if (this.debounce)
267
+ clearTimeout(this.debounce);
268
+ this.debounce = setTimeout(() => {
269
+ if (!fs.existsSync(this.dir)) {
270
+ this.watcher?.close();
271
+ this.watcher = undefined;
272
+ return;
273
+ }
274
+ void this.reload().catch((err) => logger_1.log.warn(`[PluginHost] reload error: ${err.message}`));
275
+ }, 300);
276
+ });
277
+ // init 的 reload 跑在本方法之前(当时 watcher 未建,refreshDirWatchers 早退)
278
+ // ——这里补建目录包子目录的 watcher。
279
+ this.refreshDirWatchers();
280
+ }
281
+ catch (err) {
282
+ logger_1.log.warn(`[PluginHost] watch failed: ${err.message}`);
283
+ }
284
+ }
285
+ }
286
+ exports.PluginHost = PluginHost;
287
+ function pickContributions(mod) {
288
+ const out = {};
289
+ if (mod.usage)
290
+ out.usage = mod.usage;
291
+ if (mod.media)
292
+ out.media = mod.media;
293
+ if (mod.runtime)
294
+ out.runtime = mod.runtime;
295
+ if (mod.uiTools)
296
+ out.uiTools = mod.uiTools;
297
+ return out;
298
+ }
299
+ const README_CONTENT = `# MAFW Plugin Packages
300
+
301
+ 一个插件包可以同时贡献多种能力(usage 配额 / media 引擎 / runtime / UI 工具卡)。
302
+
303
+ ## 形态
304
+
305
+ - 单文件包:\`my-vendor.js\`(包名 = 文件名)
306
+ - 目录包:\`my-vendor/plugin.json\`(\`{ "name", "version"?, "main"? }\`,main 缺省 index.js)
307
+
308
+ ## 模块形状(二选一)
309
+
310
+ \`\`\`js
311
+ module.exports = {
312
+ name: "my-vendor",
313
+ usage: { name: "my-vendor", type: "api", plan: "P", async fetch(ctx) {} },
314
+ media: { modalities: ["image"], engine: "pi", fixPayload(p) {} },
315
+ runtime: { capabilities: {}, async createRuntime(ctx) {} },
316
+ uiTools: {}, // v1 仅在插件中心展示登记,桌面侧加载仍走 ~/.mafw/ui-plugins/
317
+ };
318
+ // 或:module.exports = { name, async activate(ctx) { return { usage: {...} }; } };
319
+ \`\`\`
320
+
321
+ ## ctx(三个 legacy ctx 的超集)
322
+
323
+ - \`ctx.apiKey(provider)\` / \`ctx.fetch(url, opts)\`(60s)/ \`ctx.log\`
324
+ - \`ctx.pluginConfig()\` —— 读 config.yaml \`plugins.<name>.config\`(回退 legacy 三段 pluginConfig)
325
+ - \`ctx.projectDir\` / \`ctx.gatewayPort\` / \`ctx.usage.modelStats({sinceMs?, provider?})\`
326
+
327
+ ## 优先级
328
+
329
+ 同名时:**包 > legacy 目录文件(usage-plugins/ media-plugins/ runtime-plugins/)> 内置**。
330
+ legacy 目录行为不变。reload 只清主文件的 require.cache——改包内 lib/ 共享代码需 \`mafw restart\`。
331
+ `;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -112,9 +112,12 @@ function migrateGatewayDb(memoryDir, managerSessions, cursorFile, registrySnapsh
112
112
  logger_1.log.warn(`[gateway-db] cursor import failed: ${err.message}`);
113
113
  }
114
114
  }
115
- // ④ registry snapshot (refreshed every start)
115
+ // ④ registry snapshot (refreshed every start) — scope/key aligned with the
116
+ // live read/write path (index.ts 'registry/snapshot'/'default'); the legacy
117
+ // 'registry' scope is removed idempotently.
116
118
  if (registrySnapshot !== null) {
117
- db.kvSet('registry', 'snapshot', registrySnapshot);
119
+ db.kvSet('registry/snapshot', 'default', registrySnapshot);
120
+ db.kvDelete('registry', 'snapshot');
118
121
  result.registrySnapshot = true;
119
122
  }
120
123
  db.close();
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ // Secret redaction for the observation pipeline. Applied once at capture time
3
+ // (before t1_observations insert) so every downstream consumer (turnCompress
4
+ // transcripts, worker prompts, archives) only ever sees redacted content.
5
+ // Deterministic regex pass — no LLM, no false-negative risk beyond pattern
6
+ // coverage; deliberately conservative about false positives (prose like
7
+ // "token budget" or "the secret sauce" must survive).
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.redactSecrets = redactSecrets;
10
+ const PLACEHOLDER = '[REDACTED]';
11
+ // Ordered: specific token formats first, generic key=value forms last.
12
+ const PATTERNS = [
13
+ // Bearer tokens in Authorization-style headers
14
+ /(Bearer\s+)[A-Za-z0-9._~+/=-]{8,}/g,
15
+ // OpenAI / DashScope / OpenRouter / Anthropic style: sk-..., sk-or-v1-..., sk-ant-...
16
+ /\bsk-[A-Za-z0-9_-]{16,}\b/g,
17
+ // GitHub tokens
18
+ /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{16,}\b/g,
19
+ /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
20
+ // Google API keys
21
+ /\bAIza[0-9A-Za-z_-]{35}\b/g,
22
+ // AWS access key ids
23
+ /\bAKIA[0-9A-Z]{16}\b/g,
24
+ // Slack tokens
25
+ /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g,
26
+ // JWTs (header.payload.signature)
27
+ /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}\b/g,
28
+ // Generic key=value / "key": "value" secret forms. Keys are explicit
29
+ // (optionally wrapped in quotes); values must be >= 8 non-delimiter chars so
30
+ // prose ("password length", short numerics) is untouched. Quoted values
31
+ // keep their quotes.
32
+ // group1 keeps everything through the opening value quote; the closing
33
+ // quote stays outside the match so it survives in place.
34
+ /((?:"?)\b(?:api[_-]?key|apikey|api[_-]?secret|secret[_-]?key|access[_-]?token|auth[_-]?token|private[_-]?key|client[_-]?secret|password|passwd)\b"?\s*[=:]\s*")[^"\s]{8,}/gi,
35
+ // "[" excluded from values so an already-written [REDACTED] placeholder is
36
+ // never re-matched by this pass.
37
+ /((?:"?)\b(?:api[_-]?key|apikey|api[_-]?secret|secret[_-]?key|access[_-]?token|auth[_-]?token|private[_-]?key|client[_-]?secret|password|passwd)\b"?\s*[=:]\s*)[^\s"',\][]{8,}/gi,
38
+ ];
39
+ function redactSecrets(content) {
40
+ if (!content)
41
+ return content;
42
+ let out = content;
43
+ for (const pattern of PATTERNS) {
44
+ out = out.replace(pattern, (match, ...rest) => {
45
+ // Group-free patterns: first rest arg is the numeric offset, replace
46
+ // wholesale. Patterns with one capture group keep the captured prefix
47
+ // (e.g. "Bearer ", "api_key=").
48
+ const group1 = rest[0];
49
+ return typeof group1 === 'string' ? `${group1}${PLACEHOLDER}` : PLACEHOLDER;
50
+ });
51
+ }
52
+ return out;
53
+ }
@@ -14,6 +14,8 @@ Rules:
14
14
  - every cue_anchors list MUST include the topic entity names (project, module, person, API, feature) so the memory can be retrieved across sessions
15
15
  - for preferences or constraints, include a machine-readable anchor like "pref:<dimension>=<value>" (e.g., "pref:ui-language=chinese") in addition to the entity
16
16
  - skip redundant or trivial content; do not repeat entries that are obviously already known
17
+ - pointer over fulltext: if the durable content is already carried by a repo artifact (code, docs, ADR, issue, config), record a POINTER (path/URL + one-line gist) instead of restating the full content — the artifact is the source of truth, the memory is only its index
18
+ - for procedural memories, end memory_value with a next-time pointer when applicable: which skill, tool, or command the next agent should reach for (e.g., "→ next time: use the /tdd skill") — a memory should be a signpost, not just an archive
17
19
  - for every memory you write, include an explicit importance score via the mafw_add_memory importance parameter: 1=trivial routine, 5=ordinary fact, 9-10=architecture-level decision or serious incident
18
20
  - if nothing is worth saving, do not call the tool
19
21
  Division of labor: your job is the FACT LAYER of this session — concrete facts, decisions, preferences, event outcomes, and specific technical pitfalls (which API does what). Do NOT attempt cross-session pattern generalization — that is the daily reflection pipeline's job.
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleEventPublish = handleEventPublish;
4
+ const event_broadcast_1 = require("../runtime/event-broadcast");
5
+ function readBody(req) {
6
+ return new Promise((resolve, reject) => {
7
+ let body = '';
8
+ req.on('data', (c) => body += c);
9
+ req.on('end', () => resolve(body));
10
+ req.on('error', reject);
11
+ });
12
+ }
13
+ async function handleEventPublish(deps, req, res) {
14
+ const send = (status, payload) => {
15
+ res.writeHead(status, { 'Content-Type': 'application/json' });
16
+ res.end(JSON.stringify(payload));
17
+ };
18
+ try {
19
+ const body = JSON.parse(await readBody(req));
20
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
21
+ send(400, { error: 'body must be a JSON object' });
22
+ return;
23
+ }
24
+ if (typeof body.type !== 'string' || !body.type) {
25
+ send(400, { error: "body.type must be a non-empty string (recommended namespace: 'plugin:<name>:<event>')" });
26
+ return;
27
+ }
28
+ if (body.type === 'opencode_event') {
29
+ // 信封旁路:data 形状守卫(data.type 缺失 → 400,不发半成品事件)
30
+ if (!body.data || typeof body.data !== 'object' || typeof body.data.type !== 'string' || !body.data.type) {
31
+ send(400, { error: 'opencode_event envelope requires data.type (non-empty string)' });
32
+ return;
33
+ }
34
+ deps.broadcast((0, event_broadcast_1.opencodeBroadcast)(body.data, body.data.internal === true));
35
+ send(200, { ok: true });
36
+ return;
37
+ }
38
+ deps.broadcast(body);
39
+ send(200, { ok: true });
40
+ }
41
+ catch (err) {
42
+ send(400, { error: `invalid JSON body: ${err.message}` });
43
+ }
44
+ }