u-foo 1.0.3 → 1.0.6

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 (91) hide show
  1. package/README.md +67 -8
  2. package/README.zh-CN.md +9 -7
  3. package/SKILLS/ufoo/SKILL.md +117 -0
  4. package/SKILLS/uinit/SKILL.md +73 -0
  5. package/SKILLS/ustatus/SKILL.md +36 -0
  6. package/bin/uclaude.js +13 -0
  7. package/bin/ucodex.js +13 -0
  8. package/bin/ufoo +9 -31
  9. package/bin/ufoo.js +13 -0
  10. package/modules/AGENTS.template.md +15 -7
  11. package/modules/bus/README.md +28 -23
  12. package/modules/bus/SKILLS/ubus/SKILL.md +18 -8
  13. package/modules/context/README.md +18 -40
  14. package/modules/context/SKILLS/uctx/SKILL.md +61 -1
  15. package/package.json +16 -4
  16. package/scripts/.archived/bash-to-js-migration/README.md +46 -0
  17. package/scripts/.archived/bash-to-js-migration/banner.sh +89 -0
  18. package/scripts/{bus-inject.sh → .archived/bash-to-js-migration/bus-inject.sh} +35 -3
  19. package/scripts/{bus.sh → .archived/bash-to-js-migration/bus.sh} +3 -1
  20. package/scripts/banner.sh +2 -89
  21. package/scripts/postinstall.js +59 -0
  22. package/src/agent/cliRunner.js +33 -5
  23. package/src/agent/internalRunner.js +78 -51
  24. package/src/agent/launcher.js +702 -0
  25. package/src/agent/notifier.js +200 -0
  26. package/src/agent/ptyRunner.js +377 -0
  27. package/src/agent/ptyWrapper.js +354 -0
  28. package/src/agent/readyDetector.js +159 -0
  29. package/src/agent/ufooAgent.js +37 -42
  30. package/src/bus/API_DESIGN.md +204 -0
  31. package/src/bus/activate.js +156 -0
  32. package/src/bus/daemon.js +308 -0
  33. package/src/bus/index.js +785 -0
  34. package/src/bus/inject.js +285 -0
  35. package/src/bus/message.js +302 -0
  36. package/src/bus/nickname.js +86 -0
  37. package/src/bus/queue.js +131 -0
  38. package/src/bus/shake.js +26 -0
  39. package/src/bus/subscriber.js +296 -0
  40. package/src/bus/utils.js +357 -0
  41. package/src/chat/index.js +1842 -249
  42. package/src/cli.js +658 -95
  43. package/src/config.js +9 -2
  44. package/src/context/decisions.js +314 -0
  45. package/src/context/doctor.js +183 -0
  46. package/src/context/index.js +38 -0
  47. package/src/daemon/index.js +749 -94
  48. package/src/daemon/ops.js +395 -51
  49. package/src/daemon/providerSessions.js +291 -0
  50. package/src/daemon/run.js +34 -1
  51. package/src/daemon/status.js +24 -7
  52. package/src/doctor/index.js +50 -0
  53. package/src/init/index.js +264 -0
  54. package/src/skills/index.js +159 -0
  55. package/src/status/index.js +252 -0
  56. package/src/terminal/detect.js +64 -0
  57. package/src/terminal/index.js +8 -0
  58. package/src/terminal/iterm2.js +126 -0
  59. package/src/ufoo/agentsStore.js +41 -0
  60. package/src/ufoo/paths.js +46 -0
  61. package/src/utils/banner.js +73 -0
  62. package/bin/uclaude +0 -65
  63. package/bin/ucodex +0 -65
  64. package/modules/bus/scripts/bus-alert.sh +0 -185
  65. package/modules/bus/scripts/bus-listen.sh +0 -117
  66. package/modules/context/ASSUMPTIONS.md +0 -7
  67. package/modules/context/CONSTRAINTS.md +0 -7
  68. package/modules/context/CONTEXT-STRUCTURE.md +0 -49
  69. package/modules/context/DECISION-PROTOCOL.md +0 -62
  70. package/modules/context/HANDOFF.md +0 -33
  71. package/modules/context/RULES.md +0 -15
  72. package/modules/context/SKILLS/README.md +0 -14
  73. package/modules/context/SYSTEM.md +0 -18
  74. package/modules/context/TEMPLATES/assumptions.md +0 -4
  75. package/modules/context/TEMPLATES/constraints.md +0 -4
  76. package/modules/context/TEMPLATES/decision.md +0 -16
  77. package/modules/context/TEMPLATES/project-context-readme.md +0 -6
  78. package/modules/context/TEMPLATES/system.md +0 -3
  79. package/modules/context/TEMPLATES/terminology.md +0 -4
  80. package/modules/context/TERMINOLOGY.md +0 -10
  81. /package/scripts/{bus-alert.sh → .archived/bash-to-js-migration/bus-alert.sh} +0 -0
  82. /package/scripts/{bus-autotrigger.sh → .archived/bash-to-js-migration/bus-autotrigger.sh} +0 -0
  83. /package/scripts/{bus-daemon.sh → .archived/bash-to-js-migration/bus-daemon.sh} +0 -0
  84. /package/scripts/{bus-listen.sh → .archived/bash-to-js-migration/bus-listen.sh} +0 -0
  85. /package/scripts/{context-decisions.sh → .archived/bash-to-js-migration/context-decisions.sh} +0 -0
  86. /package/scripts/{context-doctor.sh → .archived/bash-to-js-migration/context-doctor.sh} +0 -0
  87. /package/scripts/{context-lint.sh → .archived/bash-to-js-migration/context-lint.sh} +0 -0
  88. /package/scripts/{doctor.sh → .archived/bash-to-js-migration/doctor.sh} +0 -0
  89. /package/scripts/{init.sh → .archived/bash-to-js-migration/init.sh} +0 -0
  90. /package/scripts/{skills.sh → .archived/bash-to-js-migration/skills.sh} +0 -0
  91. /package/scripts/{status.sh → .archived/bash-to-js-migration/status.sh} +0 -0
package/src/cli.js CHANGED
@@ -1,5 +1,8 @@
1
1
  const path = require("path");
2
2
  const { spawnSync } = require("child_process");
3
+ const net = require("net");
4
+ const fs = require("fs");
5
+ const { socketPath, isRunning } = require("./daemon");
3
6
 
4
7
  function getPackageRoot() {
5
8
  return path.resolve(__dirname, "..");
@@ -22,6 +25,97 @@ function getPackageScript(rel) {
22
25
  return path.join(getPackageRoot(), rel);
23
26
  }
24
27
 
28
+ function connectSocket(sockPath) {
29
+ return new Promise((resolve, reject) => {
30
+ const client = net.createConnection(sockPath, () => resolve(client));
31
+ client.on("error", reject);
32
+ });
33
+ }
34
+
35
+ async function connectWithRetry(sockPath, retries, delayMs) {
36
+ for (let i = 0; i < retries; i += 1) {
37
+ try {
38
+ // eslint-disable-next-line no-await-in-loop
39
+ const client = await connectSocket(sockPath);
40
+ return client;
41
+ } catch {
42
+ // eslint-disable-next-line no-await-in-loop
43
+ await new Promise((r) => setTimeout(r, delayMs));
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+
49
+ async function ensureDaemonRunning(projectRoot) {
50
+ if (isRunning(projectRoot)) return;
51
+ const repoRoot = getPackageRoot();
52
+ run(process.execPath, [path.join(repoRoot, "bin", "ufoo.js"), "daemon", "start"]);
53
+ const sock = socketPath(projectRoot);
54
+ for (let i = 0; i < 30; i += 1) {
55
+ if (fs.existsSync(sock)) {
56
+ return;
57
+ }
58
+ // eslint-disable-next-line no-await-in-loop
59
+ await new Promise((r) => setTimeout(r, 200));
60
+ }
61
+ }
62
+
63
+ async function sendDaemonRequest(projectRoot, payload) {
64
+ const sock = socketPath(projectRoot);
65
+ const client = await connectWithRetry(sock, 25, 200);
66
+ if (!client) {
67
+ throw new Error("Failed to connect to ufoo daemon");
68
+ }
69
+ return new Promise((resolve, reject) => {
70
+ let buffer = "";
71
+ const timeout = setTimeout(() => {
72
+ try {
73
+ client.destroy();
74
+ } catch {
75
+ // ignore
76
+ }
77
+ reject(new Error("Daemon request timeout"));
78
+ }, 8000);
79
+ const cleanup = () => {
80
+ clearTimeout(timeout);
81
+ client.removeAllListeners();
82
+ try {
83
+ client.end();
84
+ } catch {
85
+ // ignore
86
+ }
87
+ };
88
+ client.on("data", (data) => {
89
+ buffer += data.toString("utf8");
90
+ const lines = buffer.split(/\r?\n/);
91
+ buffer = lines.pop() || "";
92
+ for (const line of lines) {
93
+ if (!line.trim()) continue;
94
+ let msg;
95
+ try {
96
+ msg = JSON.parse(line);
97
+ } catch {
98
+ continue;
99
+ }
100
+ if (msg.type === "response" || msg.type === "error") {
101
+ cleanup();
102
+ if (msg.type === "error") {
103
+ reject(new Error(msg.error || "Daemon error"));
104
+ } else {
105
+ resolve(msg);
106
+ }
107
+ return;
108
+ }
109
+ }
110
+ });
111
+ client.on("error", (err) => {
112
+ cleanup();
113
+ reject(err);
114
+ });
115
+ client.write(`${JSON.stringify(payload)}\n`);
116
+ });
117
+ }
118
+
25
119
  function requireOptional(name) {
26
120
  try {
27
121
  // eslint-disable-next-line global-require, import/no-dynamic-require
@@ -44,21 +138,25 @@ async function runCli(argv) {
44
138
  program
45
139
  .name("ufoo")
46
140
  .description("ufoo CLI (wrapper-first; prefers project-local scripts).")
47
- .version(pkg.version);
141
+ .version(pkg.version, "-v, --version", "Display version with banner");
48
142
 
49
143
  program
50
144
  .command("doctor")
51
145
  .description("Run repo doctor checks")
52
146
  .action(() => {
53
147
  const repoRoot = getPackageRoot();
54
- run("bash", [path.join(repoRoot, "scripts/doctor.sh")]);
148
+ const RepoDoctor = require("./doctor");
149
+ const doctor = new RepoDoctor(repoRoot);
150
+ const ok = doctor.run();
151
+ if (!ok) process.exitCode = 1;
55
152
  });
56
153
  program
57
154
  .command("status")
58
155
  .description("Show project status (banner, unread bus, open decisions)")
59
- .action(() => {
60
- const repoRoot = getPackageRoot();
61
- run("bash", [path.join(repoRoot, "scripts/status.sh")]);
156
+ .action(async () => {
157
+ const StatusDisplay = require("./status");
158
+ const status = new StatusDisplay(process.cwd());
159
+ await status.show();
62
160
  });
63
161
  program
64
162
  .command("daemon")
@@ -81,21 +179,47 @@ async function runCli(argv) {
81
179
  const repoRoot = getPackageRoot();
82
180
  run(process.execPath, [path.join(repoRoot, "bin", "ufoo.js"), "chat"]);
83
181
  });
182
+ program
183
+ .command("resume")
184
+ .description("Resume agent sessions (optional nickname)")
185
+ .argument("[nickname]", "Nickname or subscriber ID to resume")
186
+ .action(async (nickname) => {
187
+ try {
188
+ const projectRoot = process.cwd();
189
+ await ensureDaemonRunning(projectRoot);
190
+ const resp = await sendDaemonRequest(projectRoot, {
191
+ type: "resume_agents",
192
+ target: nickname || "",
193
+ });
194
+ const reply = resp?.data?.reply || "Resume requested";
195
+ console.log(reply);
196
+ if (resp?.data?.resume?.resumed?.length) {
197
+ resp.data.resume.resumed.forEach((item) => {
198
+ const label = item.nickname ? ` (${item.nickname})` : "";
199
+ console.log(` - ${item.agent}${label}`);
200
+ });
201
+ }
202
+ } catch (err) {
203
+ console.error(err.message || String(err));
204
+ process.exitCode = 1;
205
+ }
206
+ });
84
207
 
85
208
  program
86
209
  .command("init")
87
210
  .description("Initialize modules in a project")
88
211
  .option("--modules <list>", "Comma-separated modules (context,bus,resources)", "context")
89
212
  .option("--project <dir>", "Target project directory", process.cwd())
90
- .action((opts) => {
213
+ .action(async (opts) => {
214
+ const UfooInit = require("./init");
91
215
  const repoRoot = getPackageRoot();
92
- run("bash", [
93
- path.join(repoRoot, "scripts/init.sh"),
94
- "--modules",
95
- opts.modules,
96
- "--project",
97
- opts.project,
98
- ]);
216
+ const init = new UfooInit(repoRoot);
217
+ try {
218
+ await init.init(opts);
219
+ } catch (err) {
220
+ console.error(err.message);
221
+ process.exitCode = 1;
222
+ }
99
223
  });
100
224
 
101
225
  const skills = program.command("skills").description("Manage skills templates");
@@ -103,8 +227,11 @@ async function runCli(argv) {
103
227
  .command("list")
104
228
  .description("List available skills")
105
229
  .action(() => {
230
+ const SkillsManager = require("./skills");
106
231
  const repoRoot = getPackageRoot();
107
- run("bash", [path.join(repoRoot, "scripts/skills.sh"), "list"]);
232
+ const manager = new SkillsManager(repoRoot);
233
+ const skillsList = manager.list();
234
+ skillsList.forEach((skill) => console.log(skill));
108
235
  });
109
236
  skills
110
237
  .command("install")
@@ -113,13 +240,16 @@ async function runCli(argv) {
113
240
  .option("--target <dir>", "Install target directory")
114
241
  .option("--codex", "Install into ~/.codex/skills")
115
242
  .option("--agents", "Install into ~/.agents/skills")
116
- .action((name, opts) => {
243
+ .action(async (name, opts) => {
244
+ const SkillsManager = require("./skills");
117
245
  const repoRoot = getPackageRoot();
118
- const args = [path.join(repoRoot, "scripts/skills.sh"), "install", name];
119
- if (opts.target) args.push("--target", opts.target);
120
- if (opts.codex) args.push("--codex");
121
- if (opts.agents) args.push("--agents");
122
- run("bash", args);
246
+ const manager = new SkillsManager(repoRoot);
247
+ try {
248
+ await manager.install(name, opts);
249
+ } catch (err) {
250
+ console.error(err.message);
251
+ process.exitCode = 1;
252
+ }
123
253
  });
124
254
 
125
255
  const bus = program.command("bus").description("Project bus commands");
@@ -135,29 +265,42 @@ async function runCli(argv) {
135
265
  .option("--no-bell", "Disable terminal bell")
136
266
  .allowUnknownOption(true)
137
267
  .action((subscriber, interval, opts) => {
138
- const script = getPackageScript("scripts/bus-alert.sh");
139
- const args = [script, subscriber, interval];
140
- if (opts.notify) args.push("--notify");
141
- if (opts.daemon) args.push("--daemon");
142
- if (opts.stop) args.push("--stop");
143
- if (opts.title === false) args.push("--no-title");
144
- if (opts.bell === false) args.push("--no-bell");
145
- run("bash", args);
268
+ const EventBus = require("./bus");
269
+ const eventBus = new EventBus(process.cwd());
270
+ const parsedInterval = parseInt(interval, 10);
271
+ eventBus
272
+ .alert(subscriber, Number.isFinite(parsedInterval) ? parsedInterval : 2, {
273
+ notify: opts.notify,
274
+ daemon: opts.daemon,
275
+ stop: opts.stop,
276
+ title: opts.title !== false,
277
+ bell: opts.bell !== false,
278
+ })
279
+ .catch((err) => {
280
+ console.error(err.message);
281
+ process.exitCode = 1;
282
+ });
146
283
  });
147
284
  bus
148
285
  .command("listen")
149
286
  .description("Foreground listener for incoming messages")
150
- .argument("<subscriber>", "Subscriber ID")
287
+ .argument("[subscriber]", "Subscriber ID")
151
288
  .option("--from-beginning", "Print existing queued messages first")
152
289
  .option("--reset", "Truncate pending queue before listening")
153
290
  .option("--auto-join", "Auto-join bus to get subscriber ID")
154
291
  .action((subscriber, opts) => {
155
- const script = getPackageScript("scripts/bus-listen.sh");
156
- const args = [script, subscriber];
157
- if (opts.fromBeginning) args.push("--from-beginning");
158
- if (opts.reset) args.push("--reset");
159
- if (opts.autoJoin) args.push("--auto-join");
160
- run("bash", args);
292
+ const EventBus = require("./bus");
293
+ const eventBus = new EventBus(process.cwd());
294
+ eventBus
295
+ .listen(subscriber, {
296
+ fromBeginning: opts.fromBeginning,
297
+ reset: opts.reset,
298
+ autoJoin: opts.autoJoin,
299
+ })
300
+ .catch((err) => {
301
+ console.error(err.message);
302
+ process.exitCode = 1;
303
+ });
161
304
  });
162
305
  bus
163
306
  .command("daemon")
@@ -167,56 +310,222 @@ async function runCli(argv) {
167
310
  .option("--stop", "Stop running daemon")
168
311
  .option("--status", "Check daemon status")
169
312
  .action((opts) => {
170
- const script = getPackageScript("scripts/bus-daemon.sh");
171
- const args = [script];
172
- if (opts.interval) args.push("--interval", opts.interval);
173
- if (opts.daemon) args.push("--daemon");
174
- if (opts.stop) args.push("--stop");
175
- if (opts.status) args.push("--status");
176
- run("bash", args);
313
+ const EventBus = require("./bus");
314
+ const eventBus = new EventBus(process.cwd());
315
+ (async () => {
316
+ try {
317
+ const interval = parseInt(opts.interval, 10) * 1000 || 2000;
318
+ if (opts.stop) {
319
+ await eventBus.daemon("stop");
320
+ } else if (opts.status) {
321
+ await eventBus.daemon("status");
322
+ } else {
323
+ await eventBus.daemon("start", { background: opts.daemon, interval });
324
+ }
325
+ } catch (err) {
326
+ console.error(err.message);
327
+ process.exitCode = 1;
328
+ }
329
+ })();
177
330
  });
178
331
  bus
179
332
  .command("inject")
180
- .description("Inject /bus into a Terminal.app tab by subscriber ID")
333
+ .description("Inject /ubus command into agent's terminal (via PTY socket or tmux)")
181
334
  .argument("<subscriber>", "Subscriber ID to inject into")
182
335
  .action((subscriber) => {
183
- const script = getPackageScript("scripts/bus-inject.sh");
184
- run("bash", [script, subscriber]);
336
+ const EventBus = require("./bus");
337
+ const eventBus = new EventBus(process.cwd());
338
+ (async () => {
339
+ try {
340
+ await eventBus.inject(subscriber);
341
+ } catch (err) {
342
+ console.error(err.message);
343
+ process.exitCode = 1;
344
+ }
345
+ })();
346
+ });
347
+ bus
348
+ .command("activate")
349
+ .description("Activate (focus) the terminal/tmux window of an agent")
350
+ .argument("<agent-id>", "Agent ID or nickname to activate")
351
+ .action((agentId) => {
352
+ const AgentActivator = require("./bus/activate");
353
+ const activator = new AgentActivator(process.cwd());
354
+ (async () => {
355
+ try {
356
+ await activator.activate(agentId);
357
+ } catch (err) {
358
+ console.error(err.message);
359
+ process.exitCode = 1;
360
+ }
361
+ })();
185
362
  });
186
363
  bus
187
364
  .command("run", { isDefault: true })
188
- .description("Run bus.sh commands (join, check, send, status, etc.)")
365
+ .description("Run bus commands (join, check, send, status, etc.)")
189
366
  .allowUnknownOption(true)
190
- .argument("<args...>", "Arguments passed to scripts/bus.sh")
191
- .action((args) => {
192
- const script = getPackageScript("scripts/bus.sh");
193
- run("bash", [script, ...args]);
367
+ .argument("<args...>", "Arguments passed to bus module")
368
+ .action(async (args) => {
369
+ const EventBus = require("./bus");
370
+ const eventBus = new EventBus(process.cwd());
371
+ const cmd = args[0];
372
+ const cmdArgs = args.slice(1);
373
+
374
+ try {
375
+ switch (cmd) {
376
+ case "init":
377
+ await eventBus.init();
378
+ break;
379
+ case "join":
380
+ {
381
+ const subscriber = await eventBus.join(cmdArgs[0], cmdArgs[1], cmdArgs[2]);
382
+ if (subscriber) console.log(subscriber);
383
+ }
384
+ break;
385
+ case "leave":
386
+ await eventBus.leave(cmdArgs[0]);
387
+ break;
388
+ case "send":
389
+ {
390
+ // 自动 join(如果还没有 join)并获取 subscriber ID
391
+ const publisher = await eventBus.ensureJoined();
392
+ await eventBus.send(cmdArgs[0], cmdArgs[1], publisher);
393
+ }
394
+ break;
395
+ case "broadcast":
396
+ {
397
+ // 自动 join(如果还没有 join)并获取 subscriber ID
398
+ const publisher = await eventBus.ensureJoined();
399
+ await eventBus.broadcast(cmdArgs[0], publisher);
400
+ }
401
+ break;
402
+ case "check":
403
+ await eventBus.check(cmdArgs[0]);
404
+ break;
405
+ case "ack":
406
+ await eventBus.ack(cmdArgs[0]);
407
+ break;
408
+ case "consume":
409
+ await eventBus.consume(cmdArgs[0], cmdArgs.includes("--from-beginning"));
410
+ break;
411
+ case "status":
412
+ await eventBus.status();
413
+ break;
414
+ case "resolve":
415
+ await eventBus.resolve(cmdArgs[0], cmdArgs[1]);
416
+ break;
417
+ case "rename":
418
+ await eventBus.rename(cmdArgs[0], cmdArgs[1]);
419
+ break;
420
+ case "whoami":
421
+ await eventBus.whoami();
422
+ break;
423
+ default:
424
+ console.error(`Unknown bus subcommand: ${sub}`);
425
+ process.exitCode = 1;
426
+ }
427
+ } catch (err) {
428
+ console.error(err.message);
429
+ process.exitCode = 1;
430
+ }
194
431
  });
195
432
 
196
433
  program
197
434
  .command("ctx")
198
- .description("Project ctx commands (delegates to ./scripts/context-*.sh)")
435
+ .description("Project context commands (doctor|lint|decisions)")
199
436
  .argument("[subcmd]", "Subcommand (doctor|lint|decisions)", "doctor")
200
437
  .allowUnknownOption(true)
201
438
  .argument("[subargs...]", "Subcommand args")
202
- .action((subcmd, subargs = []) => {
203
- const map = {
204
- doctor: "scripts/context-doctor.sh",
205
- lint: "scripts/context-lint.sh",
206
- decisions: "scripts/context-decisions.sh",
207
- };
208
- const rel = map[subcmd];
209
- if (!rel) {
210
- console.error(
211
- chalk.red(
212
- `Unknown ctx subcommand: ${subcmd}. Supported: ${Object.keys(map).join(", ")}`
213
- )
214
- );
439
+ .action(async (subcmd, subargs = []) => {
440
+ const DecisionsManager = require("./context/decisions");
441
+ const ContextDoctor = require("./context/doctor");
442
+ const cwd = process.cwd();
443
+
444
+ try {
445
+ switch (subcmd) {
446
+ case "doctor": {
447
+ const doctor = new ContextDoctor(cwd);
448
+ const mode = subargs.includes("--project") ? "project" : "protocol";
449
+ const projectPath = mode === "project" ? subargs[subargs.indexOf("--project") + 1] : null;
450
+ await doctor.run({ mode, projectPath });
451
+ break;
452
+ }
453
+ case "lint": {
454
+ const doctor = new ContextDoctor(cwd);
455
+ const mode = subargs.includes("--project") ? "project" : "protocol";
456
+ const projectPath = mode === "project" ? subargs[subargs.indexOf("--project") + 1] : null;
457
+ if (mode === "project") {
458
+ doctor.lintProject(projectPath);
459
+ } else {
460
+ doctor.lintProtocol();
461
+ }
462
+ break;
463
+ }
464
+ case "decisions": {
465
+ const manager = new DecisionsManager(cwd);
466
+ const opts = {};
467
+
468
+ if (subargs[0] === "index" || subargs.includes("--index")) {
469
+ manager.writeIndex();
470
+ break;
471
+ }
472
+ if (subargs[0] === "new") {
473
+ const create = { title: "", author: "", status: "" };
474
+ for (let i = 1; i < subargs.length; i++) {
475
+ const arg = subargs[i];
476
+ if (arg === "--author") {
477
+ create.author = subargs[++i] || "";
478
+ continue;
479
+ }
480
+ if (arg === "--status") {
481
+ create.status = subargs[++i] || "";
482
+ continue;
483
+ }
484
+ if (!arg.startsWith("-")) {
485
+ create.title = create.title ? `${create.title} ${arg}` : arg;
486
+ continue;
487
+ }
488
+ }
489
+ manager.createDecision(create);
490
+ break;
491
+ }
492
+
493
+ // Parse options
494
+ for (let i = 0; i < subargs.length; i++) {
495
+ if (subargs[i] === "-n") opts.num = parseInt(subargs[++i]);
496
+ if (subargs[i] === "-s") opts.status = subargs[++i];
497
+ if (subargs[i] === "-l") opts.listOnly = true;
498
+ if (subargs[i] === "-a") opts.all = true;
499
+ if (subargs[i] === "-d") {
500
+ manager.decisionsDir = subargs[++i];
501
+ manager.contextDir = path.dirname(manager.decisionsDir);
502
+ manager.indexFile = path.join(manager.contextDir, "decisions.jsonl");
503
+ }
504
+ }
505
+
506
+ if (opts.listOnly) {
507
+ manager.list({ status: opts.status || "open" });
508
+ } else {
509
+ manager.show({
510
+ status: opts.status || "open",
511
+ num: opts.num || 1,
512
+ all: opts.all || false,
513
+ });
514
+ }
515
+ break;
516
+ }
517
+ default:
518
+ console.error(
519
+ chalk.red(
520
+ `Unknown ctx subcommand: ${subcmd}. Supported: doctor, lint, decisions`
521
+ )
522
+ );
523
+ process.exitCode = 1;
524
+ }
525
+ } catch (err) {
526
+ console.error(chalk.red(`Error: ${err.message}`));
215
527
  process.exitCode = 1;
216
- return;
217
528
  }
218
- const script = getPackageScript(rel);
219
- run("bash", [script, ...subargs]);
220
529
  });
221
530
 
222
531
  program.addHelpText(
@@ -224,10 +533,17 @@ async function runCli(argv) {
224
533
  `\nNotes:\n - If 'ufoo' isn't in PATH, run it via ${chalk.cyan(
225
534
  "./bin/ufoo"
226
535
  )} (repo) or install globally via npm.\n - For bus notifications inside Codex, prefer ${chalk.cyan(
227
- "scripts/bus-alert.sh"
228
- )} / ${chalk.cyan("scripts/bus-listen.sh")} (no IME issues).\n`
536
+ "ufoo bus alert"
537
+ )} / ${chalk.cyan("ufoo bus listen")} (no IME issues).\n`
229
538
  );
230
539
 
540
+ // 检查是否是 --version 或 -V 参数
541
+ if (argv.includes("--version") || argv.includes("-V")) {
542
+ const { showUfooBanner } = require("./utils/banner");
543
+ showUfooBanner({ version: pkg.version });
544
+ return;
545
+ }
546
+
231
547
  await program.parseAsync(argv);
232
548
  return;
233
549
  }
@@ -245,14 +561,15 @@ async function runCli(argv) {
245
561
  console.log(" ufoo status");
246
562
  console.log(" ufoo daemon --start|--stop|--status");
247
563
  console.log(" ufoo chat");
564
+ console.log(" ufoo resume [nickname]");
248
565
  console.log(" ufoo init [--modules <list>] [--project <dir>]");
249
566
  console.log(" ufoo skills list");
250
567
  console.log(" ufoo skills install <name|all> [--target <dir> | --codex | --agents]");
251
- console.log(" ufoo bus <args...> (delegates to ./scripts/bus.sh)");
568
+ console.log(" ufoo bus <args...> (JS bus implementation)");
252
569
  console.log(" ufoo ctx <subcmd> ... (doctor|lint|decisions)");
253
570
  console.log("");
254
571
  console.log("Notes:");
255
- console.log(" - For Codex notifications, use scripts/bus-alert.sh / scripts/bus-listen.sh");
572
+ console.log(" - For Codex notifications, use ufoo bus alert / ufoo bus listen");
256
573
  };
257
574
 
258
575
  if (cmd === "" || cmd === "--help" || cmd === "-h") {
@@ -260,12 +577,26 @@ async function runCli(argv) {
260
577
  return;
261
578
  }
262
579
 
580
+ if (cmd === "--version" || cmd === "-V") {
581
+ const { showUfooBanner } = require("./utils/banner");
582
+ showUfooBanner({ version: pkg.version });
583
+ return;
584
+ }
585
+
263
586
  if (cmd === "doctor") {
264
- run("bash", [path.join(repoRoot, "scripts/doctor.sh")]);
587
+ const RepoDoctor = require("./doctor");
588
+ const doctor = new RepoDoctor(repoRoot);
589
+ const ok = doctor.run();
590
+ if (!ok) process.exitCode = 1;
265
591
  return;
266
592
  }
267
593
  if (cmd === "status") {
268
- run("bash", [path.join(repoRoot, "scripts/status.sh")]);
594
+ const StatusDisplay = require("./status");
595
+ const status = new StatusDisplay(process.cwd());
596
+ status.show().catch((err) => {
597
+ console.error(err.message);
598
+ process.exitCode = 1;
599
+ });
269
600
  return;
270
601
  }
271
602
  if (cmd === "daemon") {
@@ -276,32 +607,77 @@ async function runCli(argv) {
276
607
  run(process.execPath, [path.join(repoRoot, "bin", "ufoo.js"), "chat"]);
277
608
  return;
278
609
  }
610
+ if (cmd === "resume") {
611
+ const nickname = rest[0] || "";
612
+ (async () => {
613
+ try {
614
+ const projectRoot = process.cwd();
615
+ await ensureDaemonRunning(projectRoot);
616
+ const resp = await sendDaemonRequest(projectRoot, {
617
+ type: "resume_agents",
618
+ target: nickname,
619
+ });
620
+ const reply = resp?.data?.reply || "Resume requested";
621
+ console.log(reply);
622
+ } catch (err) {
623
+ console.error(err.message || String(err));
624
+ process.exitCode = 1;
625
+ }
626
+ })();
627
+ return;
628
+ }
279
629
  if (cmd === "init") {
630
+ const UfooInit = require("./init");
631
+ const init = new UfooInit(repoRoot);
632
+
280
633
  const getOpt = (name, def) => {
281
634
  const i = rest.indexOf(name);
282
635
  if (i === -1) return def;
283
636
  if (i + 1 >= rest.length) throw new Error(`Missing value for ${name}`);
284
637
  return rest[i + 1];
285
638
  };
286
- run("bash", [
287
- path.join(repoRoot, "scripts/init.sh"),
288
- "--modules",
289
- getOpt("--modules", "context"),
290
- "--project",
291
- getOpt("--project", process.cwd()),
292
- ]);
639
+
640
+ const opts = {
641
+ modules: getOpt("--modules", "context"),
642
+ project: getOpt("--project", process.cwd()),
643
+ };
644
+
645
+ init.init(opts).catch((err) => {
646
+ console.error(err.message);
647
+ process.exitCode = 1;
648
+ });
293
649
  return;
294
650
  }
295
651
  if (cmd === "skills") {
652
+ const SkillsManager = require("./skills");
653
+ const manager = new SkillsManager(repoRoot);
296
654
  const sub = rest[0] || "";
655
+
297
656
  if (sub === "list") {
298
- run("bash", [path.join(repoRoot, "scripts/skills.sh"), "list"]);
657
+ const skillsList = manager.list();
658
+ skillsList.forEach((skill) => console.log(skill));
299
659
  return;
300
660
  }
301
661
  if (sub === "install") {
302
662
  const name = rest[1];
303
663
  if (!name) throw new Error("skills install requires <name|all>");
304
- run("bash", [path.join(repoRoot, "scripts/skills.sh"), "install", ...rest.slice(1)]);
664
+
665
+ const options = {};
666
+ for (let i = 2; i < rest.length; i++) {
667
+ if (rest[i] === "--target" && i + 1 < rest.length) {
668
+ options.target = rest[i + 1];
669
+ i++;
670
+ } else if (rest[i] === "--codex") {
671
+ options.codex = true;
672
+ } else if (rest[i] === "--agents") {
673
+ options.agents = true;
674
+ }
675
+ }
676
+
677
+ manager.install(name, options).catch((err) => {
678
+ console.error(err.message);
679
+ process.exitCode = 1;
680
+ });
305
681
  return;
306
682
  }
307
683
  help();
@@ -311,34 +687,221 @@ async function runCli(argv) {
311
687
  if (cmd === "bus") {
312
688
  const sub = rest[0] || "";
313
689
  if (sub === "alert") {
314
- run("bash", [getPackageScript("scripts/bus-alert.sh"), ...rest.slice(1)]);
690
+ const EventBus = require("./bus");
691
+ const eventBus = new EventBus(process.cwd());
692
+ const args = rest.slice(1);
693
+ const subscriber = args[0];
694
+ let interval = 2;
695
+ let idx = 1;
696
+ if (args[1] && /^[0-9]+$/.test(args[1])) {
697
+ interval = parseInt(args[1], 10);
698
+ idx = 2;
699
+ }
700
+ const options = {
701
+ notify: args.includes("--notify"),
702
+ daemon: args.includes("--daemon"),
703
+ stop: args.includes("--stop"),
704
+ title: !args.includes("--no-title"),
705
+ bell: !args.includes("--no-bell"),
706
+ };
707
+ eventBus
708
+ .alert(subscriber, interval, options)
709
+ .catch((err) => {
710
+ console.error(err.message);
711
+ process.exitCode = 1;
712
+ });
315
713
  return;
316
714
  }
317
715
  if (sub === "listen") {
318
- run("bash", [getPackageScript("scripts/bus-listen.sh"), ...rest.slice(1)]);
716
+ const EventBus = require("./bus");
717
+ const eventBus = new EventBus(process.cwd());
718
+ const args = rest.slice(1);
719
+ const subscriber = args.find((arg) => !arg.startsWith("--"));
720
+ const options = {
721
+ fromBeginning: args.includes("--from-beginning"),
722
+ reset: args.includes("--reset"),
723
+ autoJoin: args.includes("--auto-join"),
724
+ };
725
+ eventBus
726
+ .listen(subscriber, options)
727
+ .catch((err) => {
728
+ console.error(err.message);
729
+ process.exitCode = 1;
730
+ });
319
731
  return;
320
732
  }
321
733
  if (sub === "daemon") {
322
- run("bash", [getPackageScript("scripts/bus-daemon.sh"), ...rest.slice(1)]);
734
+ // 使用 JavaScript daemon
735
+ const EventBus = require("./bus");
736
+ const eventBus = new EventBus(process.cwd());
737
+
738
+ (async () => {
739
+ try {
740
+ const hasStop = rest.includes("--stop");
741
+ const hasStatus = rest.includes("--status");
742
+ const hasDaemon = rest.includes("--daemon");
743
+ const intervalIdx = rest.indexOf("--interval");
744
+ const interval = intervalIdx !== -1 ? parseInt(rest[intervalIdx + 1], 10) * 1000 : 2000;
745
+
746
+ if (hasStop) {
747
+ await eventBus.daemon("stop");
748
+ } else if (hasStatus) {
749
+ await eventBus.daemon("status");
750
+ } else {
751
+ await eventBus.daemon("start", { background: hasDaemon, interval });
752
+ }
753
+ } catch (err) {
754
+ console.error(err.message);
755
+ process.exitCode = 1;
756
+ }
757
+ })();
323
758
  return;
324
759
  }
325
760
  if (sub === "inject") {
326
- run("bash", [getPackageScript("scripts/bus-inject.sh"), ...rest.slice(1)]);
761
+ // 使用 JavaScript inject
762
+ const EventBus = require("./bus");
763
+ const eventBus = new EventBus(process.cwd());
764
+
765
+ (async () => {
766
+ try {
767
+ const subscriber = rest[1];
768
+ if (!subscriber) {
769
+ throw new Error("inject requires <subscriber-id>");
770
+ }
771
+ await eventBus.inject(subscriber);
772
+ } catch (err) {
773
+ console.error(err.message);
774
+ process.exitCode = 1;
775
+ }
776
+ })();
327
777
  return;
328
778
  }
329
- run("bash", [getPackageScript("scripts/bus.sh"), ...rest]);
779
+
780
+ // Use JavaScript EventBus module for core commands
781
+ const EventBus = require("./bus");
782
+ const eventBus = new EventBus(process.cwd());
783
+
784
+ (async () => {
785
+ try {
786
+ const cmdArgs = rest.slice(1);
787
+ switch (sub) {
788
+ case "init":
789
+ await eventBus.init();
790
+ break;
791
+ case "join":
792
+ {
793
+ const subscriber = await eventBus.join(cmdArgs[0], cmdArgs[1], cmdArgs[2]);
794
+ if (subscriber) console.log(subscriber);
795
+ }
796
+ break;
797
+ case "leave":
798
+ await eventBus.leave(cmdArgs[0]);
799
+ break;
800
+ case "send":
801
+ {
802
+ // 自动 join(如果还没有 join)并获取 subscriber ID
803
+ const publisher = await eventBus.ensureJoined();
804
+ await eventBus.send(cmdArgs[0], cmdArgs[1], publisher);
805
+ }
806
+ break;
807
+ case "broadcast":
808
+ {
809
+ // 自动 join(如果还没有 join)并获取 subscriber ID
810
+ const publisher = await eventBus.ensureJoined();
811
+ await eventBus.broadcast(cmdArgs[0], publisher);
812
+ }
813
+ break;
814
+ case "check":
815
+ await eventBus.check(cmdArgs[0]);
816
+ break;
817
+ case "ack":
818
+ await eventBus.ack(cmdArgs[0]);
819
+ break;
820
+ case "consume":
821
+ await eventBus.consume(cmdArgs[0], cmdArgs.includes("--from-beginning"));
822
+ break;
823
+ case "status":
824
+ await eventBus.status();
825
+ break;
826
+ case "resolve":
827
+ await eventBus.resolve(cmdArgs[0], cmdArgs[1]);
828
+ break;
829
+ case "rename":
830
+ await eventBus.rename(cmdArgs[0], cmdArgs[1]);
831
+ break;
832
+ case "whoami":
833
+ await eventBus.whoami();
834
+ break;
835
+ default:
836
+ console.error(`Unknown bus subcommand: ${sub}`);
837
+ process.exitCode = 1;
838
+ }
839
+ } catch (err) {
840
+ console.error(err.message);
841
+ process.exitCode = 1;
842
+ }
843
+ })();
330
844
  return;
331
845
  }
332
846
  if (cmd === "ctx") {
333
847
  const sub = rest[0] || "doctor";
334
- const map = {
335
- doctor: "scripts/context-doctor.sh",
336
- lint: "scripts/context-lint.sh",
337
- decisions: "scripts/context-decisions.sh",
338
- };
339
- const rel = map[sub];
340
- if (!rel) throw new Error(`Unknown ctx subcommand: ${sub}`);
341
- run("bash", [getPackageScript(rel), ...rest.slice(1)]);
848
+ const subargs = rest.slice(1);
849
+ const DecisionsManager = require("./context/decisions");
850
+ const ContextDoctor = require("./context/doctor");
851
+ const cwd = process.cwd();
852
+
853
+ (async () => {
854
+ try {
855
+ switch (sub) {
856
+ case "doctor": {
857
+ const doctor = new ContextDoctor(cwd);
858
+ const mode = subargs.includes("--project") ? "project" : "protocol";
859
+ const projectPath = mode === "project" ? subargs[subargs.indexOf("--project") + 1] : null;
860
+ await doctor.run({ mode, projectPath });
861
+ break;
862
+ }
863
+ case "lint": {
864
+ const doctor = new ContextDoctor(cwd);
865
+ const mode = subargs.includes("--project") ? "project" : "protocol";
866
+ const projectPath = mode === "project" ? subargs[subargs.indexOf("--project") + 1] : null;
867
+ if (mode === "project") {
868
+ doctor.lintProject(projectPath);
869
+ } else {
870
+ doctor.lintProtocol();
871
+ }
872
+ break;
873
+ }
874
+ case "decisions": {
875
+ const manager = new DecisionsManager(cwd);
876
+ const opts = {};
877
+
878
+ for (let i = 0; i < subargs.length; i++) {
879
+ if (subargs[i] === "-n") opts.num = parseInt(subargs[++i]);
880
+ if (subargs[i] === "-s") opts.status = subargs[++i];
881
+ if (subargs[i] === "-l") opts.listOnly = true;
882
+ if (subargs[i] === "-a") opts.all = true;
883
+ if (subargs[i] === "-d") manager.decisionsDir = subargs[++i];
884
+ }
885
+
886
+ if (opts.listOnly) {
887
+ manager.list({ status: opts.status || "open" });
888
+ } else {
889
+ manager.show({
890
+ status: opts.status || "open",
891
+ num: opts.num || 1,
892
+ all: opts.all || false,
893
+ });
894
+ }
895
+ break;
896
+ }
897
+ default:
898
+ throw new Error(`Unknown ctx subcommand: ${sub}`);
899
+ }
900
+ } catch (err) {
901
+ console.error(`Error: ${err.message}`);
902
+ process.exit(1);
903
+ }
904
+ })();
342
905
  return;
343
906
  }
344
907