@jack200714/mafw 4.5.2 → 4.10.1

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 (37) 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 +315 -139
  6. package/gateway/dist/media/media-plugin-loader.js +25 -13
  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 +34 -0
  10. package/gateway/dist/plugins/hub.js +153 -19
  11. package/gateway/dist/plugins/package-context.js +24 -0
  12. package/gateway/dist/plugins/package-host.js +331 -0
  13. package/gateway/dist/plugins/package-types.js +2 -0
  14. package/gateway/dist/recall/gateway-db-migrate.js +5 -2
  15. package/gateway/dist/recall/redact.js +53 -0
  16. package/gateway/dist/recall/turn-pipeline.js +2 -0
  17. package/gateway/dist/routes/event-publish.js +44 -0
  18. package/gateway/dist/routes/plugins.js +19 -6
  19. package/gateway/dist/routes/waitwhat-command.js +43 -0
  20. package/gateway/dist/runtime/contract.js +4 -1
  21. package/gateway/dist/runtime/event-broadcast.js +41 -0
  22. package/gateway/dist/runtime/loader.js +45 -14
  23. package/gateway/dist/runtime/normalize.js +13 -0
  24. package/gateway/dist/runtime/pi/pi-approval-bridge.js +12 -2
  25. package/gateway/dist/runtime/pi/pi-approval-extension.js +11 -3
  26. package/gateway/dist/runtime/pi/pi-session.js +21 -3
  27. package/gateway/dist/runtime/plugins/pi-runtime.js +6 -3
  28. package/gateway/dist/runtime/serve-sidecar.js +4 -1
  29. package/gateway/dist/runtime/serve-supervisor.js +12 -0
  30. package/gateway/dist/runtime/validate.js +39 -0
  31. package/gateway/dist/skills/manager-identity.js +6 -1
  32. package/gateway/dist/usage/builtin-plugins/gateway.js +103 -26
  33. package/gateway/dist/usage/plugin-context.js +42 -2
  34. package/gateway/dist/usage/plugin-loader.js +32 -13
  35. package/gateway/package.json +2 -2
  36. package/package.json +3 -1
  37. package/packages/tui/dist/cli.js +28 -5
@@ -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
+ }
@@ -15,6 +15,14 @@ function readBody(req) {
15
15
  req.on('error', reject);
16
16
  });
17
17
  }
18
+ function readBodyBuffer(req) {
19
+ return new Promise((resolve, reject) => {
20
+ const chunks = [];
21
+ req.on('data', (c) => chunks.push(c));
22
+ req.on('end', () => resolve(Buffer.concat(chunks)));
23
+ req.on('error', reject);
24
+ });
25
+ }
18
26
  async function readJsonBody(req) {
19
27
  const raw = await readBody(req);
20
28
  try {
@@ -43,15 +51,20 @@ async function guarded(res, fn) {
43
51
  }
44
52
  }
45
53
  async function handlePluginsList(_req, res, deps) {
46
- await guarded(res, async () => ({ plugins: (0, hub_1.listPlugins)(deps.hub) }));
54
+ await guarded(res, async () => ({
55
+ plugins: (0, hub_1.listPlugins)(deps.hub),
56
+ packages: deps.hub.getPackages?.() ?? [],
57
+ }));
47
58
  }
48
59
  async function handlePluginsInstall(req, res, deps) {
49
60
  await guarded(res, async () => {
50
- const body = await readJsonBody(req);
51
- const entry = await (0, hub_1.installPlugin)(deps.hub, {
52
- type: body.type, filename: body.filename, contentBase64: body.contentBase64, overwrite: !!body.overwrite,
53
- });
54
- logger_1.log.info(`[PluginsHub] installed ${body.type}/${body.filename}`);
61
+ const url = new URL(req.url || '/', 'http://localhost');
62
+ const filename = url.searchParams.get('filename') || '';
63
+ const type = (url.searchParams.get('type') || undefined);
64
+ const overwrite = url.searchParams.get('overwrite') === '1' || url.searchParams.get('overwrite') === 'true';
65
+ const bytes = await readBodyBuffer(req);
66
+ const entry = await (0, hub_1.installPlugin)(deps.hub, { filename, type, bytes, overwrite });
67
+ logger_1.log.info(`[PluginsHub] installed ${entry.type}/${filename}`);
55
68
  return entry;
56
69
  });
57
70
  }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ // /waitwhat command (UI-driven, via /api/mafw-commands/run): the user signals
3
+ // "that last reply didn't land" and the agent re-pitches its own last message
4
+ // in simplified language, using the project's CONTEXT.md glossary when one
5
+ // exists. Inspired by mattpocock/skills wait-what (MIT).
6
+ //
7
+ // The re-pitch is prompted INTO THE SAME session so every connected client
8
+ // (desktop / TUI / opencode) sees the answer inline via the event stream.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.buildWaitwhatPrompt = buildWaitwhatPrompt;
11
+ exports.runWaitwhat = runWaitwhat;
12
+ function buildWaitwhatPrompt(original) {
13
+ return [
14
+ '[/waitwhat] 用户没看懂你上一条回复。请把它重述一遍:',
15
+ '1. 先用一两句补上"我们在做什么、刚才说到哪"的上下文定位;',
16
+ '2. 然后用简明语言重述:短句、一次一个概念、避免嵌套行话(STE100 简化技术英语的风格);',
17
+ '3. 如果项目根目录有 CONTEXT.md,先读它并用其中的项目术语(ubiquitous language)来表达;',
18
+ '4. 只是重述,不要新增内容、不引入新话题。',
19
+ '',
20
+ '—— 需要重述的上一条回复原文 ——',
21
+ original,
22
+ ].join('\n');
23
+ }
24
+ /**
25
+ * Re-pitch the last assistant message of the session. Returns ok:false with
26
+ * an error string (without prompting) when there is nothing to re-pitch.
27
+ */
28
+ async function runWaitwhat(sessionID, deps) {
29
+ const messages = await deps.listMessages(sessionID);
30
+ const lastAssistant = [...(Array.isArray(messages) ? messages : [])]
31
+ .reverse()
32
+ .find((m) => m?.info?.role === 'assistant');
33
+ const original = (lastAssistant?.parts || [])
34
+ .filter((p) => p?.type === 'text' && typeof p?.text === 'string')
35
+ .map((p) => p.text)
36
+ .join('\n')
37
+ .trim();
38
+ if (!original) {
39
+ return { ok: false, error: 'no assistant message to re-pitch' };
40
+ }
41
+ await deps.promptAsync(sessionID, buildWaitwhatPrompt(original));
42
+ return { ok: true };
43
+ }
@@ -15,7 +15,10 @@ function fullCapabilities() {
15
15
  agentProcessApi: true,
16
16
  completionApi: true,
17
17
  sessionBranchApi: true,
18
- turnBudgetApi: true,
18
+ // opencode 适配器不转发 maxTurns/maxCostUsd(SDK 无对应字段)——预算由
19
+ // gateway 侧 BudgetGuard 承担;声明 true 会让 attachBudgetGuardForGoal
20
+ // 跳过挂载,goal 预算在默认 runtime 上完全失效。
21
+ turnBudgetApi: false,
19
22
  questionApi: true,
20
23
  };
21
24
  }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ /**
3
+ * Mode A 全局广播信封构造器(MafwScheduler.broadcast 的载荷形状)。
4
+ *
5
+ * wire 契约(桌面 renderer / TUI 均按此解析):
6
+ * { type: 'opencode_event', data: { type, properties?, sessionID?, directory?, error?, internal? } }
7
+ *
8
+ * 规则:可选字段缺失时不得出现在 data 上(key 稳定性,消费方用
9
+ * `'x' in data` 判定时不受缺省值污染)。
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.opencodeBroadcast = opencodeBroadcast;
13
+ exports.projectRegisteredEvent = projectRegisteredEvent;
14
+ function opencodeBroadcast(data, internal) {
15
+ if (typeof data?.type !== 'string' || !data.type) {
16
+ // 形状守卫(fail-open):信封照常返回,但给出可定位诊断——畸形事件
17
+ // 在桌面/TUI 是"静默忽略",没有这行日志时插件作者无从排查。
18
+ // eslint-disable-next-line no-console
19
+ console.error(`[EventBroadcast] malformed opencode_event data: 'type' must be a non-empty string ` +
20
+ `(got ${JSON.stringify(data?.type)}); payload keys: ${data ? Object.keys(data).join(',') : '(null)'} ` +
21
+ `— downstream (desktop/TUI) will ignore this event`);
22
+ }
23
+ return {
24
+ type: 'opencode_event',
25
+ data: {
26
+ ...data,
27
+ ...(data.directory === undefined ? {} : { directory: data.directory }),
28
+ ...(internal ? { internal: true } : {}),
29
+ },
30
+ };
31
+ }
32
+ /**
33
+ * 项目注册广播(桌面 renderer 依此刷新 Rail 项目列表)。
34
+ *
35
+ * 顶层广播必须扁平(无 `data` 键):desktop 剥壳逻辑 `event = raw?.data || raw`
36
+ * 会把 `data` 当作 opencode_event 的内层载荷,`type` 随之丢失——与
37
+ * runtime_switched / user_question 同一约定。
38
+ */
39
+ function projectRegisteredEvent(projectDir) {
40
+ return { type: 'project_registered', projectDir };
41
+ }