@kolisachint/hoocode-agent 0.4.100 → 0.4.102

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 (86) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/cli/args.d.ts +2 -2
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +2 -2
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/agent-session.d.ts.map +1 -1
  7. package/dist/core/agent-session.js +9 -3
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/extension-mcp-servers.d.ts +33 -0
  10. package/dist/core/extension-mcp-servers.d.ts.map +1 -0
  11. package/dist/core/extension-mcp-servers.js +29 -0
  12. package/dist/core/extension-mcp-servers.js.map +1 -0
  13. package/dist/core/extensions/loader.d.ts +14 -0
  14. package/dist/core/extensions/loader.d.ts.map +1 -1
  15. package/dist/core/extensions/loader.js +37 -1
  16. package/dist/core/extensions/loader.js.map +1 -1
  17. package/dist/core/extensions/plugins/hooks-bridge.d.ts +24 -0
  18. package/dist/core/extensions/plugins/hooks-bridge.d.ts.map +1 -0
  19. package/dist/core/extensions/plugins/hooks-bridge.js +171 -0
  20. package/dist/core/extensions/plugins/hooks-bridge.js.map +1 -0
  21. package/dist/core/extensions/plugins/index.d.ts +29 -0
  22. package/dist/core/extensions/plugins/index.d.ts.map +1 -0
  23. package/dist/core/extensions/plugins/index.js +109 -0
  24. package/dist/core/extensions/plugins/index.js.map +1 -0
  25. package/dist/core/extensions/plugins/manifest.d.ts +71 -0
  26. package/dist/core/extensions/plugins/manifest.d.ts.map +1 -0
  27. package/dist/core/extensions/plugins/manifest.js +109 -0
  28. package/dist/core/extensions/plugins/manifest.js.map +1 -0
  29. package/dist/core/extensions/plugins/marketplace.d.ts +57 -0
  30. package/dist/core/extensions/plugins/marketplace.d.ts.map +1 -0
  31. package/dist/core/extensions/plugins/marketplace.js +82 -0
  32. package/dist/core/extensions/plugins/marketplace.js.map +1 -0
  33. package/dist/core/extensions/runner.d.ts +8 -0
  34. package/dist/core/extensions/runner.d.ts.map +1 -1
  35. package/dist/core/extensions/runner.js +9 -1
  36. package/dist/core/extensions/runner.js.map +1 -1
  37. package/dist/core/extensions/types.d.ts +4 -0
  38. package/dist/core/extensions/types.d.ts.map +1 -1
  39. package/dist/core/extensions/types.js.map +1 -1
  40. package/dist/core/resource-loader.d.ts +10 -0
  41. package/dist/core/resource-loader.d.ts.map +1 -1
  42. package/dist/core/resource-loader.js +46 -1
  43. package/dist/core/resource-loader.js.map +1 -1
  44. package/dist/core/scheduler.d.ts +58 -0
  45. package/dist/core/scheduler.d.ts.map +1 -0
  46. package/dist/core/scheduler.js +166 -0
  47. package/dist/core/scheduler.js.map +1 -0
  48. package/dist/core/sdk.d.ts +1 -1
  49. package/dist/core/sdk.d.ts.map +1 -1
  50. package/dist/core/sdk.js +1 -1
  51. package/dist/core/sdk.js.map +1 -1
  52. package/dist/core/settings-manager.d.ts.map +1 -1
  53. package/dist/core/settings-manager.js.map +1 -1
  54. package/dist/core/tools/browser-continue.d.ts +25 -0
  55. package/dist/core/tools/browser-continue.d.ts.map +1 -0
  56. package/dist/core/tools/{browser-resume.js → browser-continue.js} +19 -19
  57. package/dist/core/tools/browser-continue.js.map +1 -0
  58. package/dist/core/tools/{browser-flow.d.ts → browser-run.d.ts} +10 -10
  59. package/dist/core/tools/browser-run.d.ts.map +1 -0
  60. package/dist/core/tools/{browser-flow.js → browser-run.js} +29 -20
  61. package/dist/core/tools/browser-run.js.map +1 -0
  62. package/dist/core/tools/browsertools-shared.d.ts +9 -5
  63. package/dist/core/tools/browsertools-shared.d.ts.map +1 -1
  64. package/dist/core/tools/browsertools-shared.js +8 -4
  65. package/dist/core/tools/browsertools-shared.js.map +1 -1
  66. package/dist/core/tools/index.d.ts +7 -7
  67. package/dist/core/tools/index.d.ts.map +1 -1
  68. package/dist/core/tools/index.js +18 -18
  69. package/dist/core/tools/index.js.map +1 -1
  70. package/dist/extensions/core/hoo-core.d.ts +24 -0
  71. package/dist/extensions/core/hoo-core.d.ts.map +1 -1
  72. package/dist/extensions/core/hoo-core.js +411 -2
  73. package/dist/extensions/core/hoo-core.js.map +1 -1
  74. package/dist/main.d.ts.map +1 -1
  75. package/dist/main.js +2 -2
  76. package/dist/main.js.map +1 -1
  77. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  78. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  79. package/examples/extensions/sandbox/package.json +1 -1
  80. package/examples/extensions/with-deps/package.json +1 -1
  81. package/package.json +4 -4
  82. package/dist/core/tools/browser-flow.d.ts.map +0 -1
  83. package/dist/core/tools/browser-flow.js.map +0 -1
  84. package/dist/core/tools/browser-resume.d.ts +0 -25
  85. package/dist/core/tools/browser-resume.d.ts.map +0 -1
  86. package/dist/core/tools/browser-resume.js.map +0 -1
@@ -15,7 +15,7 @@
15
15
  * 2. ./.hoocode/hoo-config.json (project overrides — scalars win; arrays union)
16
16
  */
17
17
  import { spawn } from "node:child_process";
18
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18
+ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
19
19
  import { readdir } from "node:fs/promises";
20
20
  import { homedir } from "node:os";
21
21
  import { join, relative } from "node:path";
@@ -23,9 +23,12 @@ import { createInterface } from "node:readline";
23
23
  import { Text } from "@kolisachint/hoocode-tui";
24
24
  import { Type } from "typebox";
25
25
  import { getHooCodeDir } from "../../config.js";
26
- import { isToolCallEventType } from "../../core/extensions/types.js";
26
+ import { getExtensionMcpServers } from "../../core/extension-mcp-servers.js";
27
+ import { parseMarketplaceDir, readMarketplaceStore, resolvePluginSource, writeMarketplaceStore, } from "../../core/extensions/plugins/marketplace.js";
28
+ import { defineTool, isToolCallEventType } from "../../core/extensions/types.js";
27
29
  import { summarizeArgs } from "../../core/messages.js";
28
30
  import { DEFAULT_MODE, DEFAULT_MODE_PROMPTS as MODE_DEFAULTS } from "../../core/mode-prompts.js";
31
+ import { TaskScheduler } from "../../core/scheduler.js";
29
32
  import { subagentSkipMcp } from "../../core/subagent-depth.js";
30
33
  import { taskStore } from "../../core/task-store.js";
31
34
  import { blockedHostForUrl } from "../../core/tools/webtools-shared.js";
@@ -570,6 +573,15 @@ export function setupMcpLoader(pi) {
570
573
  allServerConfigs.push(serverConfig);
571
574
  }
572
575
  }
576
+ // 2b. Load from plugins/extensions that registered MCP servers during load.
577
+ for (const entry of getExtensionMcpServers()) {
578
+ for (const serverConfig of parseStandardMcpConfig({ mcpServers: entry.mcpServers }, `plugin:${entry.source}`)) {
579
+ if (seenNames.has(serverConfig.name))
580
+ continue;
581
+ seenNames.add(serverConfig.name);
582
+ allServerConfigs.push(serverConfig);
583
+ }
584
+ }
573
585
  // 3. Connect to all servers and register tools
574
586
  for (const serverConfig of allServerConfigs) {
575
587
  // Retain the config so a tool call can lazily reconnect a dropped server.
@@ -1253,6 +1265,401 @@ export function setupThinkingEscalation(pi) {
1253
1265
  });
1254
1266
  }
1255
1267
  // ============================================================================
1268
+ // /loop — cron scheduler, Cron* tools, and autonomous continuation
1269
+ // ============================================================================
1270
+ const AUTO_LOOP_DONE_TOKEN = "LOOP_DONE";
1271
+ const DEFAULT_AUTO_MAX_TURNS = 10;
1272
+ /** Convert a simple interval token ("5m", "2h", "1d") to a 5-field cron, or null. */
1273
+ function intervalToCron(token) {
1274
+ const m = /^(\d+)(m|h|d)$/.exec(token.trim());
1275
+ if (!m)
1276
+ return null;
1277
+ const n = Number(m[1]);
1278
+ if (n < 1)
1279
+ return null;
1280
+ if (m[2] === "m")
1281
+ return `*/${n} * * * *`;
1282
+ if (m[2] === "h")
1283
+ return `0 */${n} * * *`;
1284
+ return `0 0 */${n} * *`; // days
1285
+ }
1286
+ /** Pull a quoted cron expression off the front of an argument string. */
1287
+ function extractQuotedCron(args) {
1288
+ const m = /^"([^"]+)"\s*(.*)$/.exec(args.trim());
1289
+ return m ? { cron: m[1].trim(), rest: m[2].trim() } : null;
1290
+ }
1291
+ function isFiveFieldCron(expr) {
1292
+ return expr.trim().split(/\s+/).length === 5;
1293
+ }
1294
+ /** Flatten an assistant message's text blocks. */
1295
+ function assistantText(message) {
1296
+ const content = message.content;
1297
+ if (typeof content === "string")
1298
+ return content;
1299
+ if (!Array.isArray(content))
1300
+ return "";
1301
+ return content
1302
+ .filter((b) => !!b && b.type === "text")
1303
+ .map((b) => b.text)
1304
+ .join("\n");
1305
+ }
1306
+ /**
1307
+ * `/loop` schedules prompts via cron and drives autonomous continuation. The same
1308
+ * scheduler backs the agent-callable CronCreate/CronList/CronDelete tools.
1309
+ *
1310
+ * /loop "<cron>" <prompt> schedule recurring (5-field cron, local time)
1311
+ * /loop <5m|2h|1d> <prompt> schedule recurring at a simple interval
1312
+ * /loop once "<cron>" <prompt> schedule a one-shot
1313
+ * /loop list | /loop delete <id> | /loop stop
1314
+ * /loop auto [--max-turns N] <task> keep iterating until the task says LOOP_DONE
1315
+ */
1316
+ export function setupLoop(pi) {
1317
+ let scheduler;
1318
+ let auto = null;
1319
+ pi.on("session_start", (_event, ctx) => {
1320
+ if (scheduler)
1321
+ return;
1322
+ const isIdle = () => {
1323
+ try {
1324
+ return ctx.isIdle();
1325
+ }
1326
+ catch {
1327
+ return true;
1328
+ }
1329
+ };
1330
+ scheduler = new TaskScheduler({
1331
+ storePath: join(ctx.cwd, ".hoocode", "scheduled_tasks.json"),
1332
+ fire: (prompt) => pi.sendUserMessage(prompt, { deliverAs: "followUp" }),
1333
+ isIdle,
1334
+ });
1335
+ scheduler.start();
1336
+ });
1337
+ pi.on("session_shutdown", () => {
1338
+ scheduler?.stop();
1339
+ auto = null;
1340
+ });
1341
+ // Autonomous continuation: re-prompt on each agent_end until LOOP_DONE or budget.
1342
+ pi.on("agent_end", (event, ctx) => {
1343
+ if (!auto)
1344
+ return;
1345
+ if (ctx.hasPendingMessages())
1346
+ return; // user is steering — yield
1347
+ const last = [...event.messages].reverse().find((m) => m.role === "assistant");
1348
+ const text = last ? assistantText(last) : "";
1349
+ if (text.includes(AUTO_LOOP_DONE_TOKEN)) {
1350
+ auto = null;
1351
+ ctx.ui.notify("Autonomous loop complete.", "info");
1352
+ return;
1353
+ }
1354
+ if (auto.remaining <= 0) {
1355
+ auto = null;
1356
+ ctx.ui.notify("Autonomous loop stopped: max turns reached.", "warning");
1357
+ return;
1358
+ }
1359
+ auto.remaining -= 1;
1360
+ pi.sendUserMessage(`Continue working toward the goal. Reply with ${AUTO_LOOP_DONE_TOKEN} when fully complete.`, {
1361
+ deliverAs: "followUp",
1362
+ });
1363
+ });
1364
+ // ── Cron* tools (agent-callable) ──────────────────────────────────────────
1365
+ const toolText = (s) => ({ content: [{ type: "text", text: s }], details: undefined });
1366
+ pi.registerTool(defineTool({
1367
+ name: "CronCreate",
1368
+ label: "Schedule Task",
1369
+ description: "Schedule a prompt to be re-submitted on a cron schedule (5-field, local time: minute hour day-of-month month day-of-week). recurring=false fires once then deletes.",
1370
+ parameters: Type.Object({
1371
+ cron: Type.String({ description: "5-field cron expression in local time" }),
1372
+ prompt: Type.String({ description: "Prompt to enqueue at each fire time" }),
1373
+ recurring: Type.Optional(Type.Boolean({ description: "Fire repeatedly (default true) or once" })),
1374
+ }),
1375
+ async execute(_id, params) {
1376
+ if (!scheduler)
1377
+ return toolText("Scheduler not ready.");
1378
+ if (!isFiveFieldCron(params.cron))
1379
+ return toolText(`Invalid cron "${params.cron}" (need 5 fields).`);
1380
+ const task = scheduler.create({
1381
+ cron: params.cron,
1382
+ prompt: params.prompt,
1383
+ recurring: params.recurring ?? true,
1384
+ });
1385
+ return toolText(`Scheduled ${task.id}: "${task.cron}" (${task.recurring ? "recurring" : "once"})`);
1386
+ },
1387
+ }));
1388
+ pi.registerTool(defineTool({
1389
+ name: "CronList",
1390
+ label: "List Scheduled Tasks",
1391
+ description: "List all scheduled tasks (id, cron, recurring, prompt).",
1392
+ parameters: Type.Object({}),
1393
+ async execute() {
1394
+ const tasks = scheduler?.list() ?? [];
1395
+ if (tasks.length === 0)
1396
+ return toolText("No scheduled tasks.");
1397
+ return toolText(tasks
1398
+ .map((t) => `${t.id} ${t.cron} ${t.recurring ? "recurring" : "once"} ${JSON.stringify(t.prompt)}`)
1399
+ .join("\n"));
1400
+ },
1401
+ }));
1402
+ pi.registerTool(defineTool({
1403
+ name: "CronDelete",
1404
+ label: "Delete Scheduled Task",
1405
+ description: "Delete a scheduled task by id.",
1406
+ parameters: Type.Object({ id: Type.String({ description: "Task id from CronCreate/CronList" }) }),
1407
+ async execute(_id, params) {
1408
+ const removed = scheduler?.delete(params.id) ?? false;
1409
+ return toolText(removed ? `Deleted ${params.id}.` : `No task ${params.id}.`);
1410
+ },
1411
+ }));
1412
+ // ── /loop command ─────────────────────────────────────────────────────────
1413
+ pi.registerCommand("loop", {
1414
+ description: 'Schedule prompts via cron or run an autonomous loop. /loop "<cron>" <prompt> | /loop <5m|2h> <prompt> | /loop once "<cron>" <prompt> | /loop list | /loop delete <id> | /loop stop | /loop auto [--max-turns N] <task>',
1415
+ getArgumentCompletions: (prefix) => ["list", "delete", "stop", "once", "auto"]
1416
+ .filter((s) => s.startsWith(prefix))
1417
+ .map((s) => ({ value: s, label: s })),
1418
+ handler: async (args, ctx) => {
1419
+ const trimmed = args.trim();
1420
+ if (!scheduler) {
1421
+ ctx.ui.notify("Scheduler not ready yet.", "warning");
1422
+ return;
1423
+ }
1424
+ if (!trimmed || trimmed === "list") {
1425
+ const tasks = scheduler.list();
1426
+ ctx.ui.notify(tasks.length === 0
1427
+ ? auto
1428
+ ? `Autonomous loop active (${auto.remaining} turns left).`
1429
+ : "No scheduled tasks."
1430
+ : tasks.map((t) => `${t.id}: ${t.cron} ${t.recurring ? "" : "(once) "}— ${t.prompt}`).join("\n"), "info");
1431
+ return;
1432
+ }
1433
+ if (trimmed === "stop") {
1434
+ const had = scheduler.list().length > 0 || auto !== null;
1435
+ scheduler.clear();
1436
+ auto = null;
1437
+ ctx.ui.notify(had ? "Stopped all loops and scheduled tasks." : "Nothing to stop.", "info");
1438
+ return;
1439
+ }
1440
+ if (trimmed.startsWith("delete")) {
1441
+ const id = trimmed.slice("delete".length).trim();
1442
+ if (!id) {
1443
+ ctx.ui.notify("Usage: /loop delete <id>", "warning");
1444
+ return;
1445
+ }
1446
+ ctx.ui.notify(scheduler.delete(id) ? `Deleted ${id}.` : `No task ${id}.`, "info");
1447
+ return;
1448
+ }
1449
+ if (trimmed.startsWith("auto")) {
1450
+ let rest = trimmed.slice("auto".length).trim();
1451
+ let maxTurns = DEFAULT_AUTO_MAX_TURNS;
1452
+ const flag = /^--max-turns\s+(\d+)\s*(.*)$/.exec(rest);
1453
+ if (flag) {
1454
+ maxTurns = Number(flag[1]);
1455
+ rest = flag[2].trim();
1456
+ }
1457
+ if (!rest) {
1458
+ ctx.ui.notify("Usage: /loop auto [--max-turns N] <task>", "warning");
1459
+ return;
1460
+ }
1461
+ auto = { remaining: maxTurns };
1462
+ pi.sendUserMessage(`${rest}\n\n(Autonomous loop: keep working until the task is fully complete, then reply with ${AUTO_LOOP_DONE_TOKEN}.)`, { deliverAs: "followUp" });
1463
+ ctx.ui.notify(`Autonomous loop started (max ${maxTurns} turns). Stop with /loop stop.`, "info");
1464
+ return;
1465
+ }
1466
+ // Scheduling: one-shot or recurring, via quoted cron or interval token.
1467
+ let recurring = true;
1468
+ let body = trimmed;
1469
+ if (body.startsWith("once")) {
1470
+ recurring = false;
1471
+ body = body.slice("once".length).trim();
1472
+ }
1473
+ let cron = null;
1474
+ let prompt = "";
1475
+ const quoted = extractQuotedCron(body);
1476
+ if (quoted) {
1477
+ cron = quoted.cron;
1478
+ prompt = quoted.rest;
1479
+ }
1480
+ else {
1481
+ const [first, ...restWords] = body.split(/\s+/);
1482
+ cron = intervalToCron(first);
1483
+ prompt = restWords.join(" ").trim();
1484
+ }
1485
+ if (!cron || !isFiveFieldCron(cron)) {
1486
+ ctx.ui.notify('Usage: /loop "<cron>" <prompt> or /loop <5m|2h|1d> <prompt>', "warning");
1487
+ return;
1488
+ }
1489
+ if (!prompt) {
1490
+ ctx.ui.notify("Nothing to schedule — provide a prompt after the schedule.", "warning");
1491
+ return;
1492
+ }
1493
+ const task = scheduler.create({ cron, prompt, recurring });
1494
+ ctx.ui.notify(`Scheduled ${task.id}: "${cron}" ${recurring ? "recurring" : "once"} — "${prompt}". Manage with /loop list • /loop delete ${task.id}.`, "info");
1495
+ },
1496
+ });
1497
+ }
1498
+ // ============================================================================
1499
+ // /plugin — marketplace add/list + plugin install/list/remove
1500
+ // ============================================================================
1501
+ function sanitizeForDir(s) {
1502
+ return s.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 80);
1503
+ }
1504
+ function isGitSource(loc) {
1505
+ return /^https?:\/\//.test(loc) || loc.startsWith("git@") || loc.endsWith(".git");
1506
+ }
1507
+ /**
1508
+ * `/plugin` installs plugins from marketplaces (a git repo or local dir with a
1509
+ * Claude `.claude-plugin/marketplace.json` or Copilot-style `.github/marketplace.json`
1510
+ * index). Installed plugins are placed in `.hoocode/plugins/<name>` and loaded by the
1511
+ * plugin loader after a reload.
1512
+ *
1513
+ * /plugin marketplace add <git-url|path>
1514
+ * /plugin marketplace list
1515
+ * /plugin list list available plugins across marketplaces
1516
+ * /plugin install <name>
1517
+ * /plugin remove <name>
1518
+ */
1519
+ export function setupMarketplace(pi) {
1520
+ const storePath = (cwd) => join(cwd, ".hoocode", "marketplaces.json");
1521
+ const pluginsDir = (cwd) => join(cwd, ".hoocode", "plugins");
1522
+ const cacheDir = (cwd) => join(cwd, ".hoocode", "marketplace-cache");
1523
+ const findPlugin = (cwd, name) => {
1524
+ for (const record of readMarketplaceStore(storePath(cwd))) {
1525
+ const market = parseMarketplaceDir(record.dir);
1526
+ const entry = market?.plugins.find((p) => p.name === name);
1527
+ if (market && entry)
1528
+ return { market, entry };
1529
+ }
1530
+ return undefined;
1531
+ };
1532
+ pi.registerCommand("plugin", {
1533
+ description: "Manage plugin marketplaces. /plugin marketplace add <git-url|path> | /plugin marketplace list | /plugin list | /plugin install <name> | /plugin remove <name>",
1534
+ getArgumentCompletions: (prefix) => ["marketplace", "list", "install", "remove"]
1535
+ .filter((s) => s.startsWith(prefix))
1536
+ .map((s) => ({ value: s, label: s })),
1537
+ handler: async (args, ctx) => {
1538
+ const trimmed = args.trim();
1539
+ const cwd = ctx.cwd;
1540
+ // ── marketplace add / list ──────────────────────────────────────────
1541
+ if (trimmed.startsWith("marketplace")) {
1542
+ const sub = trimmed.slice("marketplace".length).trim();
1543
+ if (sub === "list" || sub === "") {
1544
+ const records = readMarketplaceStore(storePath(cwd));
1545
+ if (records.length === 0) {
1546
+ ctx.ui.notify("No marketplaces. Add one with /plugin marketplace add <git-url|path>.", "info");
1547
+ return;
1548
+ }
1549
+ const lines = records.map((r) => {
1550
+ const market = parseMarketplaceDir(r.dir);
1551
+ return `${market?.name ?? r.location} — ${market?.plugins.length ?? 0} plugin(s) [${r.location}]`;
1552
+ });
1553
+ ctx.ui.notify(lines.join("\n"), "info");
1554
+ return;
1555
+ }
1556
+ if (sub.startsWith("add")) {
1557
+ const loc = sub.slice("add".length).trim();
1558
+ if (!loc) {
1559
+ ctx.ui.notify("Usage: /plugin marketplace add <git-url|path>", "warning");
1560
+ return;
1561
+ }
1562
+ let dir;
1563
+ if (isGitSource(loc)) {
1564
+ dir = join(cacheDir(cwd), sanitizeForDir(loc));
1565
+ rmSync(dir, { recursive: true, force: true });
1566
+ mkdirSync(cacheDir(cwd), { recursive: true });
1567
+ const res = await pi.exec("git", ["clone", "--depth", "1", loc, dir]);
1568
+ if (res.code !== 0) {
1569
+ ctx.ui.notify(`Clone failed: ${res.stderr || res.stdout}`, "error");
1570
+ return;
1571
+ }
1572
+ }
1573
+ else {
1574
+ dir = resolvePluginSource(loc, cwd).kind === "local" ? join(cwd, loc) : loc;
1575
+ if (!existsSync(dir)) {
1576
+ ctx.ui.notify(`Path not found: ${dir}`, "error");
1577
+ return;
1578
+ }
1579
+ }
1580
+ const market = parseMarketplaceDir(dir);
1581
+ if (!market) {
1582
+ ctx.ui.notify("No marketplace manifest found (.claude-plugin/ or .github/marketplace.json).", "error");
1583
+ return;
1584
+ }
1585
+ const records = readMarketplaceStore(storePath(cwd)).filter((r) => r.location !== loc);
1586
+ records.push({ location: loc, dir });
1587
+ writeMarketplaceStore(storePath(cwd), records);
1588
+ ctx.ui.notify(`Added marketplace "${market.name}" (${market.plugins.length} plugin(s)).`, "info");
1589
+ return;
1590
+ }
1591
+ ctx.ui.notify("Usage: /plugin marketplace add <git-url|path> | /plugin marketplace list", "warning");
1592
+ return;
1593
+ }
1594
+ // ── list available plugins ──────────────────────────────────────────
1595
+ if (trimmed === "list" || trimmed === "") {
1596
+ const records = readMarketplaceStore(storePath(cwd));
1597
+ const lines = [];
1598
+ for (const record of records) {
1599
+ const market = parseMarketplaceDir(record.dir);
1600
+ for (const p of market?.plugins ?? []) {
1601
+ lines.push(`${p.name} — ${p.description ?? p.source}`);
1602
+ }
1603
+ }
1604
+ ctx.ui.notify(lines.length ? lines.join("\n") : "No plugins available. Add a marketplace first.", "info");
1605
+ return;
1606
+ }
1607
+ // ── install <name> ──────────────────────────────────────────────────
1608
+ if (trimmed.startsWith("install")) {
1609
+ const name = trimmed.slice("install".length).trim();
1610
+ if (!name) {
1611
+ ctx.ui.notify("Usage: /plugin install <name>", "warning");
1612
+ return;
1613
+ }
1614
+ const found = findPlugin(cwd, name);
1615
+ if (!found) {
1616
+ ctx.ui.notify(`Plugin "${name}" not found in any marketplace.`, "error");
1617
+ return;
1618
+ }
1619
+ const resolved = resolvePluginSource(found.entry.source, found.market.root);
1620
+ const dest = join(pluginsDir(cwd), sanitizeForDir(name));
1621
+ rmSync(dest, { recursive: true, force: true });
1622
+ mkdirSync(pluginsDir(cwd), { recursive: true });
1623
+ if (resolved.kind === "local") {
1624
+ cpSync(resolved.path, dest, { recursive: true });
1625
+ }
1626
+ else if (resolved.kind === "git") {
1627
+ const res = await pi.exec("git", ["clone", "--depth", "1", resolved.url, dest]);
1628
+ if (res.code !== 0) {
1629
+ ctx.ui.notify(`Clone failed: ${res.stderr || res.stdout}`, "error");
1630
+ return;
1631
+ }
1632
+ }
1633
+ else {
1634
+ ctx.ui.notify(`npm plugin sources are not supported yet (${resolved.spec}).`, "warning");
1635
+ return;
1636
+ }
1637
+ ctx.ui.notify(`Installed "${name}" — reloading…`, "info");
1638
+ await ctx.reload();
1639
+ return;
1640
+ }
1641
+ // ── remove <name> ───────────────────────────────────────────────────
1642
+ if (trimmed.startsWith("remove")) {
1643
+ const name = trimmed.slice("remove".length).trim();
1644
+ if (!name) {
1645
+ ctx.ui.notify("Usage: /plugin remove <name>", "warning");
1646
+ return;
1647
+ }
1648
+ const dest = join(pluginsDir(cwd), sanitizeForDir(name));
1649
+ if (!existsSync(dest)) {
1650
+ ctx.ui.notify(`Plugin "${name}" is not installed.`, "info");
1651
+ return;
1652
+ }
1653
+ rmSync(dest, { recursive: true, force: true });
1654
+ ctx.ui.notify(`Removed "${name}" — reloading…`, "info");
1655
+ await ctx.reload();
1656
+ return;
1657
+ }
1658
+ ctx.ui.notify("Usage: /plugin marketplace add|list | /plugin list | /plugin install <name> | /plugin remove <name>", "warning");
1659
+ },
1660
+ });
1661
+ }
1662
+ // ============================================================================
1256
1663
  // Extension entry point
1257
1664
  // ============================================================================
1258
1665
  function hooCore(pi) {
@@ -1262,6 +1669,8 @@ function hooCore(pi) {
1262
1669
  setupScaffold(pi);
1263
1670
  setupAskOptions(pi);
1264
1671
  setupThinkingEscalation(pi);
1672
+ setupLoop(pi);
1673
+ setupMarketplace(pi);
1265
1674
  }
1266
1675
  hooCore.displayName = "hoo-core";
1267
1676
  export default hooCore;