@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.
- package/CHANGELOG.md +20 -0
- package/dist/cli/args.d.ts +2 -2
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +2 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +9 -3
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extension-mcp-servers.d.ts +33 -0
- package/dist/core/extension-mcp-servers.d.ts.map +1 -0
- package/dist/core/extension-mcp-servers.js +29 -0
- package/dist/core/extension-mcp-servers.js.map +1 -0
- package/dist/core/extensions/loader.d.ts +14 -0
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +37 -1
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/plugins/hooks-bridge.d.ts +24 -0
- package/dist/core/extensions/plugins/hooks-bridge.d.ts.map +1 -0
- package/dist/core/extensions/plugins/hooks-bridge.js +171 -0
- package/dist/core/extensions/plugins/hooks-bridge.js.map +1 -0
- package/dist/core/extensions/plugins/index.d.ts +29 -0
- package/dist/core/extensions/plugins/index.d.ts.map +1 -0
- package/dist/core/extensions/plugins/index.js +109 -0
- package/dist/core/extensions/plugins/index.js.map +1 -0
- package/dist/core/extensions/plugins/manifest.d.ts +71 -0
- package/dist/core/extensions/plugins/manifest.d.ts.map +1 -0
- package/dist/core/extensions/plugins/manifest.js +109 -0
- package/dist/core/extensions/plugins/manifest.js.map +1 -0
- package/dist/core/extensions/plugins/marketplace.d.ts +57 -0
- package/dist/core/extensions/plugins/marketplace.d.ts.map +1 -0
- package/dist/core/extensions/plugins/marketplace.js +82 -0
- package/dist/core/extensions/plugins/marketplace.js.map +1 -0
- package/dist/core/extensions/runner.d.ts +8 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +9 -1
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +4 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/resource-loader.d.ts +10 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +46 -1
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/scheduler.d.ts +58 -0
- package/dist/core/scheduler.d.ts.map +1 -0
- package/dist/core/scheduler.js +166 -0
- package/dist/core/scheduler.js.map +1 -0
- package/dist/core/sdk.d.ts +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/browser-continue.d.ts +25 -0
- package/dist/core/tools/browser-continue.d.ts.map +1 -0
- package/dist/core/tools/{browser-resume.js → browser-continue.js} +19 -19
- package/dist/core/tools/browser-continue.js.map +1 -0
- package/dist/core/tools/{browser-flow.d.ts → browser-run.d.ts} +10 -10
- package/dist/core/tools/browser-run.d.ts.map +1 -0
- package/dist/core/tools/{browser-flow.js → browser-run.js} +29 -20
- package/dist/core/tools/browser-run.js.map +1 -0
- package/dist/core/tools/browsertools-shared.d.ts +9 -5
- package/dist/core/tools/browsertools-shared.d.ts.map +1 -1
- package/dist/core/tools/browsertools-shared.js +8 -4
- package/dist/core/tools/browsertools-shared.js.map +1 -1
- package/dist/core/tools/index.d.ts +7 -7
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +18 -18
- package/dist/core/tools/index.js.map +1 -1
- package/dist/extensions/core/hoo-core.d.ts +24 -0
- package/dist/extensions/core/hoo-core.d.ts.map +1 -1
- package/dist/extensions/core/hoo-core.js +411 -2
- package/dist/extensions/core/hoo-core.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -2
- package/dist/main.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
- package/dist/core/tools/browser-flow.d.ts.map +0 -1
- package/dist/core/tools/browser-flow.js.map +0 -1
- package/dist/core/tools/browser-resume.d.ts +0 -25
- package/dist/core/tools/browser-resume.d.ts.map +0 -1
- 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 {
|
|
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;
|