@kolisachint/hoocode-agent 0.4.131 → 0.4.132

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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.132] - 2026-07-14
4
+
3
5
  ## [0.4.131] - 2026-07-14
4
6
 
5
7
  ## [0.4.130] - 2026-07-14
@@ -1 +1 @@
1
- {"version":3,"file":"ask-options.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/ask-options.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAe,YAAY,EAAuC,MAAM,gCAAgC,CAAC;AA+BrH,wBAAgB,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAqEtD","sourcesContent":["/**\n * Options pane — the ask_options tool.\n *\n * The model calls this tool when it needs the user to make a decision before\n * continuing. Each question is shown in an inline options pane where the user\n * moves with up/down, advances with right, and may type a custom answer.\n */\n\nimport type { AgentToolResult, AgentToolUpdateCallback } from \"@kolisachint/hoocode-agent-core\";\nimport { type Static, Type } from \"typebox\";\nimport type { AskQuestion, ExtensionAPI, ExtensionContext, SessionStartEvent } from \"../../core/extensions/types.js\";\n\nconst askOptionsSchema = Type.Object({\n\tquestions: Type.Array(\n\t\tType.Object({\n\t\t\tquestion: Type.String({ description: \"The question to ask the user.\" }),\n\t\t\tdetail: Type.Optional(Type.String({ description: \"Optional clarifying sub-text shown under the question.\" })),\n\t\t\toptions: Type.Array(\n\t\t\t\tType.Object({\n\t\t\t\t\tlabel: Type.String({ description: \"The option text; returned verbatim when chosen.\" }),\n\t\t\t\t\tdescription: Type.Optional(\n\t\t\t\t\t\tType.String({ description: \"Optional short description shown next to the option.\" }),\n\t\t\t\t\t),\n\t\t\t\t\trecommended: Type.Optional(\n\t\t\t\t\t\tType.Boolean({\n\t\t\t\t\t\t\tdescription: \"When true, the option is marked '(recommended)' to help the user choose.\",\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t}),\n\t\t\t\t{ description: \"The options the user can choose from.\" },\n\t\t\t),\n\t\t\tallow_custom: Type.Optional(\n\t\t\t\tType.Boolean({\n\t\t\t\t\tdescription: \"When true, the user can type a free-form answer instead of choosing an option.\",\n\t\t\t\t}),\n\t\t\t),\n\t\t}),\n\t\t{ description: \"One or more decisions to ask the user, in order.\" },\n\t),\n});\n\nexport function setupAskOptions(pi: ExtensionAPI): void {\n\t// Capture the latest context so the tool can reach the interactive UI.\n\tlet activeCtx: ExtensionContext | undefined;\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tactiveCtx = ctx;\n\t});\n\n\tpi.registerTool({\n\t\tname: \"ask_options\",\n\t\tlabel: \"Ask the user\",\n\t\tdescription:\n\t\t\t\"Ask the user to make one or more decisions before continuing. Each question is presented \" +\n\t\t\t\"in an interactive options pane where the user selects an option (or types a custom answer). \" +\n\t\t\t\"Use this when you genuinely need input to proceed and cannot reasonably decide yourself. \" +\n\t\t\t\"Returns the user's answer for each question; if the user skips, no answers are returned.\",\n\t\tparameters: askOptionsSchema,\n\t\tasync execute(\n\t\t\t_toolCallId: string,\n\t\t\tparams: Static<typeof askOptionsSchema>,\n\t\t\tsignal: AbortSignal,\n\t\t\t_onUpdate: AgentToolUpdateCallback,\n\t\t): Promise<AgentToolResult<undefined>> {\n\t\t\tif (!activeCtx || !activeCtx.hasUI) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"Cannot ask the user: no interactive UI is available in this session. Proceed using your best judgement.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!params.questions.length) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"No questions were provided.\" }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst questions: AskQuestion[] = params.questions.map((q) => ({\n\t\t\t\tquestion: q.question,\n\t\t\t\tdetail: q.detail,\n\t\t\t\toptions: q.options.map((o) => ({ label: o.label, description: o.description, recommended: o.recommended })),\n\t\t\t\tallowCustom: q.allow_custom,\n\t\t\t}));\n\n\t\t\tconst answers = await activeCtx.ui.askOptions(questions, { signal });\n\n\t\t\tif (!answers) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"The user skipped the question(s) without answering. Ask how they would like to proceed.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst text = questions.map((q, i) => `${q.question}\\n → ${answers[i] ?? \"(no answer)\"}`).join(\"\\n\\n\");\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t});\n}\n"]}
1
+ {"version":3,"file":"ask-options.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/ask-options.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAe,YAAY,EAAuC,MAAM,gCAAgC,CAAC;AAgCrH,wBAAgB,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAiHtD","sourcesContent":["/**\n * Options pane — the ask_options tool.\n *\n * The model calls this tool when it needs the user to make a decision before\n * continuing. Each question is shown in an inline options pane where the user\n * moves with up/down, advances with right, and may type a custom answer.\n */\n\nimport type { AgentToolResult, AgentToolUpdateCallback } from \"@kolisachint/hoocode-agent-core\";\nimport { type Static, Type } from \"typebox\";\nimport type { AskQuestion, ExtensionAPI, ExtensionContext, SessionStartEvent } from \"../../core/extensions/types.js\";\nimport { LOOP_AUTO_CHANGED, LOOP_HALT } from \"./loop.js\";\n\nconst askOptionsSchema = Type.Object({\n\tquestions: Type.Array(\n\t\tType.Object({\n\t\t\tquestion: Type.String({ description: \"The question to ask the user.\" }),\n\t\t\tdetail: Type.Optional(Type.String({ description: \"Optional clarifying sub-text shown under the question.\" })),\n\t\t\toptions: Type.Array(\n\t\t\t\tType.Object({\n\t\t\t\t\tlabel: Type.String({ description: \"The option text; returned verbatim when chosen.\" }),\n\t\t\t\t\tdescription: Type.Optional(\n\t\t\t\t\t\tType.String({ description: \"Optional short description shown next to the option.\" }),\n\t\t\t\t\t),\n\t\t\t\t\trecommended: Type.Optional(\n\t\t\t\t\t\tType.Boolean({\n\t\t\t\t\t\t\tdescription: \"When true, the option is marked '(recommended)' to help the user choose.\",\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t}),\n\t\t\t\t{ description: \"The options the user can choose from.\" },\n\t\t\t),\n\t\t\tallow_custom: Type.Optional(\n\t\t\t\tType.Boolean({\n\t\t\t\t\tdescription: \"When true, the user can type a free-form answer instead of choosing an option.\",\n\t\t\t\t}),\n\t\t\t),\n\t\t}),\n\t\t{ description: \"One or more decisions to ask the user, in order.\" },\n\t),\n});\n\nexport function setupAskOptions(pi: ExtensionAPI): void {\n\t// Capture the latest context so the tool can reach the interactive UI.\n\tlet activeCtx: ExtensionContext | undefined;\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tactiveCtx = ctx;\n\t});\n\n\t// Track whether an autonomous /loop is running so we never block on a human\n\t// who isn't there. The loop extension broadcasts this on the shared bus.\n\tlet autoLoopActive = false;\n\tpi.events.on(LOOP_AUTO_CHANGED, (data) => {\n\t\tautoLoopActive = !!(data as { active?: boolean })?.active;\n\t});\n\n\tpi.registerTool({\n\t\tname: \"ask_options\",\n\t\tlabel: \"Ask the user\",\n\t\tdescription:\n\t\t\t\"Ask the user to make one or more decisions before continuing. Each question is presented \" +\n\t\t\t\"in an interactive options pane where the user selects an option (or types a custom answer). \" +\n\t\t\t\"Use this when you genuinely need input to proceed and cannot reasonably decide yourself. \" +\n\t\t\t\"Returns the user's answer for each question; if the user skips, no answers are returned.\",\n\t\tparameters: askOptionsSchema,\n\t\tasync execute(\n\t\t\t_toolCallId: string,\n\t\t\tparams: Static<typeof askOptionsSchema>,\n\t\t\tsignal: AbortSignal,\n\t\t\t_onUpdate: AgentToolUpdateCallback,\n\t\t): Promise<AgentToolResult<undefined>> {\n\t\t\tif (!activeCtx || !activeCtx.hasUI) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"Cannot ask the user: no interactive UI is available in this session. Proceed using your best judgement.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!params.questions.length) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"No questions were provided.\" }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Autonomous loop: no human is watching, so never block on the pane.\n\t\t\t// Decide any question that carries a recommended default and proceed;\n\t\t\t// if any question lacks one, there is no safe default — halt the loop\n\t\t\t// and let the model report the blocker instead of guessing.\n\t\t\tif (autoLoopActive) {\n\t\t\t\tconst blockers = params.questions.filter((q) => !q.options.some((o) => o.recommended));\n\t\t\t\tif (blockers.length) {\n\t\t\t\t\tconst list = blockers.map((q) => ` • ${q.question}`).join(\"\\n\");\n\t\t\t\t\tpi.events.emit(LOOP_HALT, {\n\t\t\t\t\t\treason: `ask_options had ${blockers.length} question(s) with no recommended default.`,\n\t\t\t\t\t});\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\ttext:\n\t\t\t\t\t\t\t\t\t`Autonomous loop: no user is available to answer, and ${blockers.length} question(s) have ` +\n\t\t\t\t\t\t\t\t\t`no recommended default to fall back on:\\n${list}\\n\\n` +\n\t\t\t\t\t\t\t\t\t`The loop has been stopped. Do not guess — stop and report this blocker to the user, ` +\n\t\t\t\t\t\t\t\t\t`explaining what decision is needed and the options you were weighing.`,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tconst text = params.questions\n\t\t\t\t\t.map((q) => {\n\t\t\t\t\t\tconst rec = q.options.find((o) => o.recommended);\n\t\t\t\t\t\treturn `${q.question}\\n → ${rec?.label} (auto-selected recommended default; autonomous loop, no user present)`;\n\t\t\t\t\t})\n\t\t\t\t\t.join(\"\\n\\n\");\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst questions: AskQuestion[] = params.questions.map((q) => ({\n\t\t\t\tquestion: q.question,\n\t\t\t\tdetail: q.detail,\n\t\t\t\toptions: q.options.map((o) => ({ label: o.label, description: o.description, recommended: o.recommended })),\n\t\t\t\tallowCustom: q.allow_custom,\n\t\t\t}));\n\n\t\t\tconst answers = await activeCtx.ui.askOptions(questions, { signal });\n\n\t\t\tif (!answers) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"The user skipped the question(s) without answering. Ask how they would like to proceed.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst text = questions.map((q, i) => `${q.question}\\n → ${answers[i] ?? \"(no answer)\"}`).join(\"\\n\\n\");\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t});\n}\n"]}
@@ -6,6 +6,7 @@
6
6
  * moves with up/down, advances with right, and may type a custom answer.
7
7
  */
8
8
  import { Type } from "typebox";
9
+ import { LOOP_AUTO_CHANGED, LOOP_HALT } from "./loop.js";
9
10
  const askOptionsSchema = Type.Object({
10
11
  questions: Type.Array(Type.Object({
11
12
  question: Type.String({ description: "The question to ask the user." }),
@@ -28,6 +29,12 @@ export function setupAskOptions(pi) {
28
29
  pi.on("session_start", (_event, ctx) => {
29
30
  activeCtx = ctx;
30
31
  });
32
+ // Track whether an autonomous /loop is running so we never block on a human
33
+ // who isn't there. The loop extension broadcasts this on the shared bus.
34
+ let autoLoopActive = false;
35
+ pi.events.on(LOOP_AUTO_CHANGED, (data) => {
36
+ autoLoopActive = !!data?.active;
37
+ });
31
38
  pi.registerTool({
32
39
  name: "ask_options",
33
40
  label: "Ask the user",
@@ -54,6 +61,41 @@ export function setupAskOptions(pi) {
54
61
  details: undefined,
55
62
  };
56
63
  }
64
+ // Autonomous loop: no human is watching, so never block on the pane.
65
+ // Decide any question that carries a recommended default and proceed;
66
+ // if any question lacks one, there is no safe default — halt the loop
67
+ // and let the model report the blocker instead of guessing.
68
+ if (autoLoopActive) {
69
+ const blockers = params.questions.filter((q) => !q.options.some((o) => o.recommended));
70
+ if (blockers.length) {
71
+ const list = blockers.map((q) => ` • ${q.question}`).join("\n");
72
+ pi.events.emit(LOOP_HALT, {
73
+ reason: `ask_options had ${blockers.length} question(s) with no recommended default.`,
74
+ });
75
+ return {
76
+ content: [
77
+ {
78
+ type: "text",
79
+ text: `Autonomous loop: no user is available to answer, and ${blockers.length} question(s) have ` +
80
+ `no recommended default to fall back on:\n${list}\n\n` +
81
+ `The loop has been stopped. Do not guess — stop and report this blocker to the user, ` +
82
+ `explaining what decision is needed and the options you were weighing.`,
83
+ },
84
+ ],
85
+ details: undefined,
86
+ };
87
+ }
88
+ const text = params.questions
89
+ .map((q) => {
90
+ const rec = q.options.find((o) => o.recommended);
91
+ return `${q.question}\n → ${rec?.label} (auto-selected recommended default; autonomous loop, no user present)`;
92
+ })
93
+ .join("\n\n");
94
+ return {
95
+ content: [{ type: "text", text }],
96
+ details: undefined,
97
+ };
98
+ }
57
99
  const questions = params.questions.map((q) => ({
58
100
  question: q.question,
59
101
  detail: q.detail,
@@ -1 +1 @@
1
- {"version":3,"file":"ask-options.js","sourceRoot":"","sources":["../../../src/extensions/core/ask-options.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Options pane — the ask_options tool.\n *\n * The model calls this tool when it needs the user to make a decision before\n * continuing. Each question is shown in an inline options pane where the user\n * moves with up/down, advances with right, and may type a custom answer.\n */\n\nimport type { AgentToolResult, AgentToolUpdateCallback } from \"@kolisachint/hoocode-agent-core\";\nimport { type Static, Type } from \"typebox\";\nimport type { AskQuestion, ExtensionAPI, ExtensionContext, SessionStartEvent } from \"../../core/extensions/types.js\";\n\nconst askOptionsSchema = Type.Object({\n\tquestions: Type.Array(\n\t\tType.Object({\n\t\t\tquestion: Type.String({ description: \"The question to ask the user.\" }),\n\t\t\tdetail: Type.Optional(Type.String({ description: \"Optional clarifying sub-text shown under the question.\" })),\n\t\t\toptions: Type.Array(\n\t\t\t\tType.Object({\n\t\t\t\t\tlabel: Type.String({ description: \"The option text; returned verbatim when chosen.\" }),\n\t\t\t\t\tdescription: Type.Optional(\n\t\t\t\t\t\tType.String({ description: \"Optional short description shown next to the option.\" }),\n\t\t\t\t\t),\n\t\t\t\t\trecommended: Type.Optional(\n\t\t\t\t\t\tType.Boolean({\n\t\t\t\t\t\t\tdescription: \"When true, the option is marked '(recommended)' to help the user choose.\",\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t}),\n\t\t\t\t{ description: \"The options the user can choose from.\" },\n\t\t\t),\n\t\t\tallow_custom: Type.Optional(\n\t\t\t\tType.Boolean({\n\t\t\t\t\tdescription: \"When true, the user can type a free-form answer instead of choosing an option.\",\n\t\t\t\t}),\n\t\t\t),\n\t\t}),\n\t\t{ description: \"One or more decisions to ask the user, in order.\" },\n\t),\n});\n\nexport function setupAskOptions(pi: ExtensionAPI): void {\n\t// Capture the latest context so the tool can reach the interactive UI.\n\tlet activeCtx: ExtensionContext | undefined;\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tactiveCtx = ctx;\n\t});\n\n\tpi.registerTool({\n\t\tname: \"ask_options\",\n\t\tlabel: \"Ask the user\",\n\t\tdescription:\n\t\t\t\"Ask the user to make one or more decisions before continuing. Each question is presented \" +\n\t\t\t\"in an interactive options pane where the user selects an option (or types a custom answer). \" +\n\t\t\t\"Use this when you genuinely need input to proceed and cannot reasonably decide yourself. \" +\n\t\t\t\"Returns the user's answer for each question; if the user skips, no answers are returned.\",\n\t\tparameters: askOptionsSchema,\n\t\tasync execute(\n\t\t\t_toolCallId: string,\n\t\t\tparams: Static<typeof askOptionsSchema>,\n\t\t\tsignal: AbortSignal,\n\t\t\t_onUpdate: AgentToolUpdateCallback,\n\t\t): Promise<AgentToolResult<undefined>> {\n\t\t\tif (!activeCtx || !activeCtx.hasUI) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"Cannot ask the user: no interactive UI is available in this session. Proceed using your best judgement.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!params.questions.length) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"No questions were provided.\" }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst questions: AskQuestion[] = params.questions.map((q) => ({\n\t\t\t\tquestion: q.question,\n\t\t\t\tdetail: q.detail,\n\t\t\t\toptions: q.options.map((o) => ({ label: o.label, description: o.description, recommended: o.recommended })),\n\t\t\t\tallowCustom: q.allow_custom,\n\t\t\t}));\n\n\t\t\tconst answers = await activeCtx.ui.askOptions(questions, { signal });\n\n\t\t\tif (!answers) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"The user skipped the question(s) without answering. Ask how they would like to proceed.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst text = questions.map((q, i) => `${q.question}\\n → ${answers[i] ?? \"(no answer)\"}`).join(\"\\n\\n\");\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t});\n}\n"]}
1
+ {"version":3,"file":"ask-options.js","sourceRoot":"","sources":["../../../src/extensions/core/ask-options.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Options pane — the ask_options tool.\n *\n * The model calls this tool when it needs the user to make a decision before\n * continuing. Each question is shown in an inline options pane where the user\n * moves with up/down, advances with right, and may type a custom answer.\n */\n\nimport type { AgentToolResult, AgentToolUpdateCallback } from \"@kolisachint/hoocode-agent-core\";\nimport { type Static, Type } from \"typebox\";\nimport type { AskQuestion, ExtensionAPI, ExtensionContext, SessionStartEvent } from \"../../core/extensions/types.js\";\nimport { LOOP_AUTO_CHANGED, LOOP_HALT } from \"./loop.js\";\n\nconst askOptionsSchema = Type.Object({\n\tquestions: Type.Array(\n\t\tType.Object({\n\t\t\tquestion: Type.String({ description: \"The question to ask the user.\" }),\n\t\t\tdetail: Type.Optional(Type.String({ description: \"Optional clarifying sub-text shown under the question.\" })),\n\t\t\toptions: Type.Array(\n\t\t\t\tType.Object({\n\t\t\t\t\tlabel: Type.String({ description: \"The option text; returned verbatim when chosen.\" }),\n\t\t\t\t\tdescription: Type.Optional(\n\t\t\t\t\t\tType.String({ description: \"Optional short description shown next to the option.\" }),\n\t\t\t\t\t),\n\t\t\t\t\trecommended: Type.Optional(\n\t\t\t\t\t\tType.Boolean({\n\t\t\t\t\t\t\tdescription: \"When true, the option is marked '(recommended)' to help the user choose.\",\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t}),\n\t\t\t\t{ description: \"The options the user can choose from.\" },\n\t\t\t),\n\t\t\tallow_custom: Type.Optional(\n\t\t\t\tType.Boolean({\n\t\t\t\t\tdescription: \"When true, the user can type a free-form answer instead of choosing an option.\",\n\t\t\t\t}),\n\t\t\t),\n\t\t}),\n\t\t{ description: \"One or more decisions to ask the user, in order.\" },\n\t),\n});\n\nexport function setupAskOptions(pi: ExtensionAPI): void {\n\t// Capture the latest context so the tool can reach the interactive UI.\n\tlet activeCtx: ExtensionContext | undefined;\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tactiveCtx = ctx;\n\t});\n\n\t// Track whether an autonomous /loop is running so we never block on a human\n\t// who isn't there. The loop extension broadcasts this on the shared bus.\n\tlet autoLoopActive = false;\n\tpi.events.on(LOOP_AUTO_CHANGED, (data) => {\n\t\tautoLoopActive = !!(data as { active?: boolean })?.active;\n\t});\n\n\tpi.registerTool({\n\t\tname: \"ask_options\",\n\t\tlabel: \"Ask the user\",\n\t\tdescription:\n\t\t\t\"Ask the user to make one or more decisions before continuing. Each question is presented \" +\n\t\t\t\"in an interactive options pane where the user selects an option (or types a custom answer). \" +\n\t\t\t\"Use this when you genuinely need input to proceed and cannot reasonably decide yourself. \" +\n\t\t\t\"Returns the user's answer for each question; if the user skips, no answers are returned.\",\n\t\tparameters: askOptionsSchema,\n\t\tasync execute(\n\t\t\t_toolCallId: string,\n\t\t\tparams: Static<typeof askOptionsSchema>,\n\t\t\tsignal: AbortSignal,\n\t\t\t_onUpdate: AgentToolUpdateCallback,\n\t\t): Promise<AgentToolResult<undefined>> {\n\t\t\tif (!activeCtx || !activeCtx.hasUI) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"Cannot ask the user: no interactive UI is available in this session. Proceed using your best judgement.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!params.questions.length) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"No questions were provided.\" }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Autonomous loop: no human is watching, so never block on the pane.\n\t\t\t// Decide any question that carries a recommended default and proceed;\n\t\t\t// if any question lacks one, there is no safe default — halt the loop\n\t\t\t// and let the model report the blocker instead of guessing.\n\t\t\tif (autoLoopActive) {\n\t\t\t\tconst blockers = params.questions.filter((q) => !q.options.some((o) => o.recommended));\n\t\t\t\tif (blockers.length) {\n\t\t\t\t\tconst list = blockers.map((q) => ` • ${q.question}`).join(\"\\n\");\n\t\t\t\t\tpi.events.emit(LOOP_HALT, {\n\t\t\t\t\t\treason: `ask_options had ${blockers.length} question(s) with no recommended default.`,\n\t\t\t\t\t});\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\ttext:\n\t\t\t\t\t\t\t\t\t`Autonomous loop: no user is available to answer, and ${blockers.length} question(s) have ` +\n\t\t\t\t\t\t\t\t\t`no recommended default to fall back on:\\n${list}\\n\\n` +\n\t\t\t\t\t\t\t\t\t`The loop has been stopped. Do not guess — stop and report this blocker to the user, ` +\n\t\t\t\t\t\t\t\t\t`explaining what decision is needed and the options you were weighing.`,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tconst text = params.questions\n\t\t\t\t\t.map((q) => {\n\t\t\t\t\t\tconst rec = q.options.find((o) => o.recommended);\n\t\t\t\t\t\treturn `${q.question}\\n → ${rec?.label} (auto-selected recommended default; autonomous loop, no user present)`;\n\t\t\t\t\t})\n\t\t\t\t\t.join(\"\\n\\n\");\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst questions: AskQuestion[] = params.questions.map((q) => ({\n\t\t\t\tquestion: q.question,\n\t\t\t\tdetail: q.detail,\n\t\t\t\toptions: q.options.map((o) => ({ label: o.label, description: o.description, recommended: o.recommended })),\n\t\t\t\tallowCustom: q.allow_custom,\n\t\t\t}));\n\n\t\t\tconst answers = await activeCtx.ui.askOptions(questions, { signal });\n\n\t\t\tif (!answers) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"The user skipped the question(s) without answering. Ask how they would like to proceed.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst text = questions.map((q, i) => `${q.question}\\n → ${answers[i] ?? \"(no answer)\"}`).join(\"\\n\\n\");\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t});\n}\n"]}
@@ -11,5 +11,17 @@
11
11
  * /loop auto [--max-turns N] <task> keep iterating until the task says LOOP_DONE
12
12
  */
13
13
  import type { ExtensionAPI } from "../../core/extensions/types.js";
14
+ /**
15
+ * Event-bus channel: the autonomous-loop active state changed.
16
+ * Payload: `{ active: boolean }`. Emitted whenever `/loop auto` starts or stops
17
+ * so other extensions (e.g. ask_options) can adapt to running unattended.
18
+ */
19
+ export declare const LOOP_AUTO_CHANGED = "loop:auto-changed";
20
+ /**
21
+ * Event-bus channel: request to halt the autonomous loop.
22
+ * Payload: `{ reason: string }`. Sent by another extension when it hits a
23
+ * blocker that requires a human decision the loop cannot safely make on its own.
24
+ */
25
+ export declare const LOOP_HALT = "loop:halt";
14
26
  export declare function setupLoop(pi: ExtensionAPI): void;
15
27
  //# sourceMappingURL=loop.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EACX,YAAY,EAIZ,MAAM,gCAAgC,CAAC;AAuCxC,wBAAgB,SAAS,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAoNhD","sourcesContent":["/**\n * /loop — cron scheduler, Cron* tools, and autonomous continuation.\n *\n * `/loop` schedules prompts via cron and drives autonomous continuation. The same\n * scheduler backs the agent-callable CronCreate/CronList/CronDelete tools.\n *\n * /loop \"<cron>\" <prompt> schedule recurring (5-field cron, local time)\n * /loop <5m|2h|1d> <prompt> schedule recurring at a simple interval\n * /loop once \"<cron>\" <prompt> schedule a one-shot\n * /loop list | /loop delete <id> | /loop stop\n * /loop auto [--max-turns N] <task> keep iterating until the task says LOOP_DONE\n */\n\nimport { join } from \"node:path\";\nimport { Type } from \"typebox\";\nimport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tSessionStartEvent,\n} from \"../../core/extensions/types.js\";\nimport { defineTool } from \"../../core/extensions/types.js\";\nimport { TaskScheduler } from \"../../core/scheduler.js\";\n\nconst AUTO_LOOP_DONE_TOKEN = \"LOOP_DONE\";\nconst DEFAULT_AUTO_MAX_TURNS = 10;\n\n/** Convert a simple interval token (\"5m\", \"2h\", \"1d\") to a 5-field cron, or null. */\nfunction intervalToCron(token: string): string | null {\n\tconst m = /^(\\d+)(m|h|d)$/.exec(token.trim());\n\tif (!m) return null;\n\tconst n = Number(m[1]);\n\tif (n < 1) return null;\n\tif (m[2] === \"m\") return `*/${n} * * * *`;\n\tif (m[2] === \"h\") return `0 */${n} * * *`;\n\treturn `0 0 */${n} * *`; // days\n}\n\n/** Pull a quoted cron expression off the front of an argument string. */\nfunction extractQuotedCron(args: string): { cron: string; rest: string } | null {\n\tconst m = /^\"([^\"]+)\"\\s*(.*)$/.exec(args.trim());\n\treturn m ? { cron: m[1].trim(), rest: m[2].trim() } : null;\n}\n\nfunction isFiveFieldCron(expr: string): boolean {\n\treturn expr.trim().split(/\\s+/).length === 5;\n}\n\n/** Flatten an assistant message's text blocks. */\nfunction assistantText(message: { content: unknown }): string {\n\tconst content = message.content;\n\tif (typeof content === \"string\") return content;\n\tif (!Array.isArray(content)) return \"\";\n\treturn content\n\t\t.filter((b): b is { type: \"text\"; text: string } => !!b && (b as { type?: string }).type === \"text\")\n\t\t.map((b) => b.text)\n\t\t.join(\"\\n\");\n}\n\nexport function setupLoop(pi: ExtensionAPI): void {\n\tlet scheduler: TaskScheduler | undefined;\n\tlet auto: { remaining: number } | null = null;\n\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tif (scheduler) return;\n\t\tconst isIdle = () => {\n\t\t\ttry {\n\t\t\t\treturn ctx.isIdle();\n\t\t\t} catch {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t};\n\t\tscheduler = new TaskScheduler({\n\t\t\t// `.agents/` is the primary, cross-vendor home; the legacy `.hoocode/`\n\t\t\t// store is read once and migrates forward on the next persist.\n\t\t\tstorePath: join(ctx.cwd, \".agents\", \"scheduled_tasks.json\"),\n\t\t\tlegacyStorePath: join(ctx.cwd, \".hoocode\", \"scheduled_tasks.json\"),\n\t\t\tfire: (prompt) => pi.sendUserMessage(prompt, { deliverAs: \"followUp\" }),\n\t\t\tisIdle,\n\t\t});\n\t\tscheduler.start();\n\t});\n\n\tpi.on(\"session_shutdown\", () => {\n\t\tscheduler?.stop();\n\t\tauto = null;\n\t});\n\n\t// Autonomous continuation: re-prompt on each agent_end until LOOP_DONE or budget.\n\tpi.on(\"agent_end\", (event, ctx) => {\n\t\tif (!auto) return;\n\t\tif (ctx.hasPendingMessages()) return; // user is steering — yield\n\t\tconst last = [...event.messages].reverse().find((m) => m.role === \"assistant\");\n\t\tconst text = last ? assistantText(last) : \"\";\n\t\tif (text.includes(AUTO_LOOP_DONE_TOKEN)) {\n\t\t\tauto = null;\n\t\t\tctx.ui.notify(\"Autonomous loop complete.\", \"info\");\n\t\t\treturn;\n\t\t}\n\t\tif (auto.remaining <= 0) {\n\t\t\tauto = null;\n\t\t\tctx.ui.notify(\"Autonomous loop stopped: max turns reached.\", \"warning\");\n\t\t\treturn;\n\t\t}\n\t\tauto.remaining -= 1;\n\t\tpi.sendUserMessage(`Continue working toward the goal. Reply with ${AUTO_LOOP_DONE_TOKEN} when fully complete.`, {\n\t\t\tdeliverAs: \"followUp\",\n\t\t});\n\t});\n\n\t// ── Cron* tools (agent-callable) ──────────────────────────────────────────\n\tconst toolText = (s: string) => ({ content: [{ type: \"text\" as const, text: s }], details: undefined });\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronCreate\",\n\t\t\tlabel: \"Schedule Task\",\n\t\t\tdescription:\n\t\t\t\t\"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.\",\n\t\t\tparameters: Type.Object({\n\t\t\t\tcron: Type.String({ description: \"5-field cron expression in local time\" }),\n\t\t\t\tprompt: Type.String({ description: \"Prompt to enqueue at each fire time\" }),\n\t\t\t\trecurring: Type.Optional(Type.Boolean({ description: \"Fire repeatedly (default true) or once\" })),\n\t\t\t}),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tif (!scheduler) return toolText(\"Scheduler not ready.\");\n\t\t\t\tif (!isFiveFieldCron(params.cron)) return toolText(`Invalid cron \"${params.cron}\" (need 5 fields).`);\n\t\t\t\tconst task = scheduler.create({\n\t\t\t\t\tcron: params.cron,\n\t\t\t\t\tprompt: params.prompt,\n\t\t\t\t\trecurring: params.recurring ?? true,\n\t\t\t\t});\n\t\t\t\treturn toolText(`Scheduled ${task.id}: \"${task.cron}\" (${task.recurring ? \"recurring\" : \"once\"})`);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronList\",\n\t\t\tlabel: \"List Scheduled Tasks\",\n\t\t\tdescription: \"List all scheduled tasks (id, cron, recurring, prompt).\",\n\t\t\tparameters: Type.Object({}),\n\t\t\tasync execute() {\n\t\t\t\tconst tasks = scheduler?.list() ?? [];\n\t\t\t\tif (tasks.length === 0) return toolText(\"No scheduled tasks.\");\n\t\t\t\treturn toolText(\n\t\t\t\t\ttasks\n\t\t\t\t\t\t.map((t) => `${t.id} ${t.cron} ${t.recurring ? \"recurring\" : \"once\"} ${JSON.stringify(t.prompt)}`)\n\t\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronDelete\",\n\t\t\tlabel: \"Delete Scheduled Task\",\n\t\t\tdescription: \"Delete a scheduled task by id.\",\n\t\t\tparameters: Type.Object({ id: Type.String({ description: \"Task id from CronCreate/CronList\" }) }),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tconst removed = scheduler?.delete(params.id) ?? false;\n\t\t\t\treturn toolText(removed ? `Deleted ${params.id}.` : `No task ${params.id}.`);\n\t\t\t},\n\t\t}),\n\t);\n\n\t// ── /loop command ─────────────────────────────────────────────────────────\n\tpi.registerCommand(\"loop\", {\n\t\tdescription:\n\t\t\t'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>',\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\t[\"list\", \"delete\", \"stop\", \"once\", \"auto\"]\n\t\t\t\t.filter((s) => s.startsWith(prefix))\n\t\t\t\t.map((s) => ({ value: s, label: s })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst trimmed = args.trim();\n\t\t\tif (!scheduler) {\n\t\t\t\tctx.ui.notify(\"Scheduler not ready yet.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!trimmed || trimmed === \"list\") {\n\t\t\t\tconst tasks = scheduler.list();\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\ttasks.length === 0\n\t\t\t\t\t\t? auto\n\t\t\t\t\t\t\t? `Autonomous loop active (${auto.remaining} turns left).`\n\t\t\t\t\t\t\t: \"No scheduled tasks.\"\n\t\t\t\t\t\t: tasks.map((t) => `${t.id}: ${t.cron} ${t.recurring ? \"\" : \"(once) \"}— ${t.prompt}`).join(\"\\n\"),\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed === \"stop\") {\n\t\t\t\tconst had = scheduler.list().length > 0 || auto !== null;\n\t\t\t\tscheduler.clear();\n\t\t\t\tauto = null;\n\t\t\t\tctx.ui.notify(had ? \"Stopped all loops and scheduled tasks.\" : \"Nothing to stop.\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"delete\")) {\n\t\t\t\tconst id = trimmed.slice(\"delete\".length).trim();\n\t\t\t\tif (!id) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop delete <id>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tctx.ui.notify(scheduler.delete(id) ? `Deleted ${id}.` : `No task ${id}.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"auto\")) {\n\t\t\t\tlet rest = trimmed.slice(\"auto\".length).trim();\n\t\t\t\tlet maxTurns = DEFAULT_AUTO_MAX_TURNS;\n\t\t\t\tconst flag = /^--max-turns\\s+(\\d+)\\s*(.*)$/.exec(rest);\n\t\t\t\tif (flag) {\n\t\t\t\t\tmaxTurns = Number(flag[1]);\n\t\t\t\t\trest = flag[2].trim();\n\t\t\t\t}\n\t\t\t\tif (!rest) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop auto [--max-turns N] <task>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tauto = { remaining: maxTurns };\n\t\t\t\tpi.sendUserMessage(\n\t\t\t\t\t`${rest}\\n\\n(Autonomous loop: keep working until the task is fully complete, then reply with ${AUTO_LOOP_DONE_TOKEN}.)`,\n\t\t\t\t\t{ deliverAs: \"followUp\" },\n\t\t\t\t);\n\t\t\t\tctx.ui.notify(`Autonomous loop started (max ${maxTurns} turns). Stop with /loop stop.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Scheduling: one-shot or recurring, via quoted cron or interval token.\n\t\t\tlet recurring = true;\n\t\t\tlet body = trimmed;\n\t\t\tif (body.startsWith(\"once\")) {\n\t\t\t\trecurring = false;\n\t\t\t\tbody = body.slice(\"once\".length).trim();\n\t\t\t}\n\n\t\t\tlet cron: string | null = null;\n\t\t\tlet prompt = \"\";\n\t\t\tconst quoted = extractQuotedCron(body);\n\t\t\tif (quoted) {\n\t\t\t\tcron = quoted.cron;\n\t\t\t\tprompt = quoted.rest;\n\t\t\t} else {\n\t\t\t\tconst [first, ...restWords] = body.split(/\\s+/);\n\t\t\t\tcron = intervalToCron(first);\n\t\t\t\tprompt = restWords.join(\" \").trim();\n\t\t\t}\n\n\t\t\tif (!cron || !isFiveFieldCron(cron)) {\n\t\t\t\tctx.ui.notify('Usage: /loop \"<cron>\" <prompt> or /loop <5m|2h|1d> <prompt>', \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!prompt) {\n\t\t\t\tctx.ui.notify(\"Nothing to schedule — provide a prompt after the schedule.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst task = scheduler.create({ cron, prompt, recurring });\n\t\t\tctx.ui.notify(\n\t\t\t\t`Scheduled ${task.id}: \"${cron}\" ${recurring ? \"recurring\" : \"once\"} — \"${prompt}\". Manage with /loop list • /loop delete ${task.id}.`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
1
+ {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EACX,YAAY,EAIZ,MAAM,gCAAgC,CAAC;AAOxC;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,sBAAsB,CAAC;AAErD;;;;GAIG;AACH,eAAO,MAAM,SAAS,cAAc,CAAC;AAkCrC,wBAAgB,SAAS,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAsOhD","sourcesContent":["/**\n * /loop — cron scheduler, Cron* tools, and autonomous continuation.\n *\n * `/loop` schedules prompts via cron and drives autonomous continuation. The same\n * scheduler backs the agent-callable CronCreate/CronList/CronDelete tools.\n *\n * /loop \"<cron>\" <prompt> schedule recurring (5-field cron, local time)\n * /loop <5m|2h|1d> <prompt> schedule recurring at a simple interval\n * /loop once \"<cron>\" <prompt> schedule a one-shot\n * /loop list | /loop delete <id> | /loop stop\n * /loop auto [--max-turns N] <task> keep iterating until the task says LOOP_DONE\n */\n\nimport { join } from \"node:path\";\nimport { Type } from \"typebox\";\nimport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tSessionStartEvent,\n} from \"../../core/extensions/types.js\";\nimport { defineTool } from \"../../core/extensions/types.js\";\nimport { TaskScheduler } from \"../../core/scheduler.js\";\n\nconst AUTO_LOOP_DONE_TOKEN = \"LOOP_DONE\";\nconst DEFAULT_AUTO_MAX_TURNS = 10;\n\n/**\n * Event-bus channel: the autonomous-loop active state changed.\n * Payload: `{ active: boolean }`. Emitted whenever `/loop auto` starts or stops\n * so other extensions (e.g. ask_options) can adapt to running unattended.\n */\nexport const LOOP_AUTO_CHANGED = \"loop:auto-changed\";\n\n/**\n * Event-bus channel: request to halt the autonomous loop.\n * Payload: `{ reason: string }`. Sent by another extension when it hits a\n * blocker that requires a human decision the loop cannot safely make on its own.\n */\nexport const LOOP_HALT = \"loop:halt\";\n\n/** Convert a simple interval token (\"5m\", \"2h\", \"1d\") to a 5-field cron, or null. */\nfunction intervalToCron(token: string): string | null {\n\tconst m = /^(\\d+)(m|h|d)$/.exec(token.trim());\n\tif (!m) return null;\n\tconst n = Number(m[1]);\n\tif (n < 1) return null;\n\tif (m[2] === \"m\") return `*/${n} * * * *`;\n\tif (m[2] === \"h\") return `0 */${n} * * *`;\n\treturn `0 0 */${n} * *`; // days\n}\n\n/** Pull a quoted cron expression off the front of an argument string. */\nfunction extractQuotedCron(args: string): { cron: string; rest: string } | null {\n\tconst m = /^\"([^\"]+)\"\\s*(.*)$/.exec(args.trim());\n\treturn m ? { cron: m[1].trim(), rest: m[2].trim() } : null;\n}\n\nfunction isFiveFieldCron(expr: string): boolean {\n\treturn expr.trim().split(/\\s+/).length === 5;\n}\n\n/** Flatten an assistant message's text blocks. */\nfunction assistantText(message: { content: unknown }): string {\n\tconst content = message.content;\n\tif (typeof content === \"string\") return content;\n\tif (!Array.isArray(content)) return \"\";\n\treturn content\n\t\t.filter((b): b is { type: \"text\"; text: string } => !!b && (b as { type?: string }).type === \"text\")\n\t\t.map((b) => b.text)\n\t\t.join(\"\\n\");\n}\n\nexport function setupLoop(pi: ExtensionAPI): void {\n\tlet scheduler: TaskScheduler | undefined;\n\tlet auto: { remaining: number } | null = null;\n\tlet activeCtx: ExtensionContext | undefined;\n\n\t/** Set the autonomous-loop state and broadcast the active flag on the bus. */\n\tfunction setAuto(next: { remaining: number } | null): void {\n\t\tconst was = auto !== null;\n\t\tauto = next;\n\t\tif (was !== (next !== null)) pi.events.emit(LOOP_AUTO_CHANGED, { active: next !== null });\n\t}\n\n\t// Another extension (e.g. ask_options) hit a decision it cannot safely make\n\t// while unattended. Stop iterating and let the model report the blocker.\n\tpi.events.on(LOOP_HALT, (data) => {\n\t\tif (!auto) return;\n\t\tconst reason = (data as { reason?: string })?.reason?.trim() || \"a decision that needs the user.\";\n\t\tsetAuto(null);\n\t\tactiveCtx?.ui.notify(`Autonomous loop halted: ${reason}`, \"warning\");\n\t});\n\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tactiveCtx = ctx;\n\t\tif (scheduler) return;\n\t\tconst isIdle = () => {\n\t\t\ttry {\n\t\t\t\treturn ctx.isIdle();\n\t\t\t} catch {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t};\n\t\tscheduler = new TaskScheduler({\n\t\t\t// `.agents/` is the primary, cross-vendor home; the legacy `.hoocode/`\n\t\t\t// store is read once and migrates forward on the next persist.\n\t\t\tstorePath: join(ctx.cwd, \".agents\", \"scheduled_tasks.json\"),\n\t\t\tlegacyStorePath: join(ctx.cwd, \".hoocode\", \"scheduled_tasks.json\"),\n\t\t\tfire: (prompt) => pi.sendUserMessage(prompt, { deliverAs: \"followUp\" }),\n\t\t\tisIdle,\n\t\t});\n\t\tscheduler.start();\n\t});\n\n\tpi.on(\"session_shutdown\", () => {\n\t\tscheduler?.stop();\n\t\tsetAuto(null);\n\t});\n\n\t// Autonomous continuation: re-prompt on each agent_end until LOOP_DONE or budget.\n\tpi.on(\"agent_end\", (event, ctx) => {\n\t\tif (!auto) return;\n\t\tif (ctx.hasPendingMessages()) return; // user is steering — yield\n\t\tconst last = [...event.messages].reverse().find((m) => m.role === \"assistant\");\n\t\tconst text = last ? assistantText(last) : \"\";\n\t\tif (text.includes(AUTO_LOOP_DONE_TOKEN)) {\n\t\t\tsetAuto(null);\n\t\t\tctx.ui.notify(\"Autonomous loop complete.\", \"info\");\n\t\t\treturn;\n\t\t}\n\t\tif (auto.remaining <= 0) {\n\t\t\tsetAuto(null);\n\t\t\tctx.ui.notify(\"Autonomous loop stopped: max turns reached.\", \"warning\");\n\t\t\treturn;\n\t\t}\n\t\tauto.remaining -= 1;\n\t\tpi.sendUserMessage(`Continue working toward the goal. Reply with ${AUTO_LOOP_DONE_TOKEN} when fully complete.`, {\n\t\t\tdeliverAs: \"followUp\",\n\t\t});\n\t});\n\n\t// ── Cron* tools (agent-callable) ──────────────────────────────────────────\n\tconst toolText = (s: string) => ({ content: [{ type: \"text\" as const, text: s }], details: undefined });\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronCreate\",\n\t\t\tlabel: \"Schedule Task\",\n\t\t\tdescription:\n\t\t\t\t\"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.\",\n\t\t\tparameters: Type.Object({\n\t\t\t\tcron: Type.String({ description: \"5-field cron expression in local time\" }),\n\t\t\t\tprompt: Type.String({ description: \"Prompt to enqueue at each fire time\" }),\n\t\t\t\trecurring: Type.Optional(Type.Boolean({ description: \"Fire repeatedly (default true) or once\" })),\n\t\t\t}),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tif (!scheduler) return toolText(\"Scheduler not ready.\");\n\t\t\t\tif (!isFiveFieldCron(params.cron)) return toolText(`Invalid cron \"${params.cron}\" (need 5 fields).`);\n\t\t\t\tconst task = scheduler.create({\n\t\t\t\t\tcron: params.cron,\n\t\t\t\t\tprompt: params.prompt,\n\t\t\t\t\trecurring: params.recurring ?? true,\n\t\t\t\t});\n\t\t\t\treturn toolText(`Scheduled ${task.id}: \"${task.cron}\" (${task.recurring ? \"recurring\" : \"once\"})`);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronList\",\n\t\t\tlabel: \"List Scheduled Tasks\",\n\t\t\tdescription: \"List all scheduled tasks (id, cron, recurring, prompt).\",\n\t\t\tparameters: Type.Object({}),\n\t\t\tasync execute() {\n\t\t\t\tconst tasks = scheduler?.list() ?? [];\n\t\t\t\tif (tasks.length === 0) return toolText(\"No scheduled tasks.\");\n\t\t\t\treturn toolText(\n\t\t\t\t\ttasks\n\t\t\t\t\t\t.map((t) => `${t.id} ${t.cron} ${t.recurring ? \"recurring\" : \"once\"} ${JSON.stringify(t.prompt)}`)\n\t\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronDelete\",\n\t\t\tlabel: \"Delete Scheduled Task\",\n\t\t\tdescription: \"Delete a scheduled task by id.\",\n\t\t\tparameters: Type.Object({ id: Type.String({ description: \"Task id from CronCreate/CronList\" }) }),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tconst removed = scheduler?.delete(params.id) ?? false;\n\t\t\t\treturn toolText(removed ? `Deleted ${params.id}.` : `No task ${params.id}.`);\n\t\t\t},\n\t\t}),\n\t);\n\n\t// ── /loop command ─────────────────────────────────────────────────────────\n\tpi.registerCommand(\"loop\", {\n\t\tdescription:\n\t\t\t'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>',\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\t[\"list\", \"delete\", \"stop\", \"once\", \"auto\"]\n\t\t\t\t.filter((s) => s.startsWith(prefix))\n\t\t\t\t.map((s) => ({ value: s, label: s })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst trimmed = args.trim();\n\t\t\tif (!scheduler) {\n\t\t\t\tctx.ui.notify(\"Scheduler not ready yet.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!trimmed || trimmed === \"list\") {\n\t\t\t\tconst tasks = scheduler.list();\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\ttasks.length === 0\n\t\t\t\t\t\t? auto\n\t\t\t\t\t\t\t? `Autonomous loop active (${auto.remaining} turns left).`\n\t\t\t\t\t\t\t: \"No scheduled tasks.\"\n\t\t\t\t\t\t: tasks.map((t) => `${t.id}: ${t.cron} ${t.recurring ? \"\" : \"(once) \"}— ${t.prompt}`).join(\"\\n\"),\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed === \"stop\") {\n\t\t\t\tconst had = scheduler.list().length > 0 || auto !== null;\n\t\t\t\tscheduler.clear();\n\t\t\t\tsetAuto(null);\n\t\t\t\tctx.ui.notify(had ? \"Stopped all loops and scheduled tasks.\" : \"Nothing to stop.\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"delete\")) {\n\t\t\t\tconst id = trimmed.slice(\"delete\".length).trim();\n\t\t\t\tif (!id) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop delete <id>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tctx.ui.notify(scheduler.delete(id) ? `Deleted ${id}.` : `No task ${id}.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"auto\")) {\n\t\t\t\tlet rest = trimmed.slice(\"auto\".length).trim();\n\t\t\t\tlet maxTurns = DEFAULT_AUTO_MAX_TURNS;\n\t\t\t\tconst flag = /^--max-turns\\s+(\\d+)\\s*(.*)$/.exec(rest);\n\t\t\t\tif (flag) {\n\t\t\t\t\tmaxTurns = Number(flag[1]);\n\t\t\t\t\trest = flag[2].trim();\n\t\t\t\t}\n\t\t\t\tif (!rest) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop auto [--max-turns N] <task>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsetAuto({ remaining: maxTurns });\n\t\t\t\tpi.sendUserMessage(\n\t\t\t\t\t`${rest}\\n\\n(Autonomous loop: keep working until the task is fully complete, then reply with ${AUTO_LOOP_DONE_TOKEN}.)`,\n\t\t\t\t\t{ deliverAs: \"followUp\" },\n\t\t\t\t);\n\t\t\t\tctx.ui.notify(`Autonomous loop started (max ${maxTurns} turns). Stop with /loop stop.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Scheduling: one-shot or recurring, via quoted cron or interval token.\n\t\t\tlet recurring = true;\n\t\t\tlet body = trimmed;\n\t\t\tif (body.startsWith(\"once\")) {\n\t\t\t\trecurring = false;\n\t\t\t\tbody = body.slice(\"once\".length).trim();\n\t\t\t}\n\n\t\t\tlet cron: string | null = null;\n\t\t\tlet prompt = \"\";\n\t\t\tconst quoted = extractQuotedCron(body);\n\t\t\tif (quoted) {\n\t\t\t\tcron = quoted.cron;\n\t\t\t\tprompt = quoted.rest;\n\t\t\t} else {\n\t\t\t\tconst [first, ...restWords] = body.split(/\\s+/);\n\t\t\t\tcron = intervalToCron(first);\n\t\t\t\tprompt = restWords.join(\" \").trim();\n\t\t\t}\n\n\t\t\tif (!cron || !isFiveFieldCron(cron)) {\n\t\t\t\tctx.ui.notify('Usage: /loop \"<cron>\" <prompt> or /loop <5m|2h|1d> <prompt>', \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!prompt) {\n\t\t\t\tctx.ui.notify(\"Nothing to schedule — provide a prompt after the schedule.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst task = scheduler.create({ cron, prompt, recurring });\n\t\t\tctx.ui.notify(\n\t\t\t\t`Scheduled ${task.id}: \"${cron}\" ${recurring ? \"recurring\" : \"once\"} — \"${prompt}\". Manage with /loop list • /loop delete ${task.id}.`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
@@ -16,6 +16,18 @@ import { defineTool } from "../../core/extensions/types.js";
16
16
  import { TaskScheduler } from "../../core/scheduler.js";
17
17
  const AUTO_LOOP_DONE_TOKEN = "LOOP_DONE";
18
18
  const DEFAULT_AUTO_MAX_TURNS = 10;
19
+ /**
20
+ * Event-bus channel: the autonomous-loop active state changed.
21
+ * Payload: `{ active: boolean }`. Emitted whenever `/loop auto` starts or stops
22
+ * so other extensions (e.g. ask_options) can adapt to running unattended.
23
+ */
24
+ export const LOOP_AUTO_CHANGED = "loop:auto-changed";
25
+ /**
26
+ * Event-bus channel: request to halt the autonomous loop.
27
+ * Payload: `{ reason: string }`. Sent by another extension when it hits a
28
+ * blocker that requires a human decision the loop cannot safely make on its own.
29
+ */
30
+ export const LOOP_HALT = "loop:halt";
19
31
  /** Convert a simple interval token ("5m", "2h", "1d") to a 5-field cron, or null. */
20
32
  function intervalToCron(token) {
21
33
  const m = /^(\d+)(m|h|d)$/.exec(token.trim());
@@ -53,7 +65,25 @@ function assistantText(message) {
53
65
  export function setupLoop(pi) {
54
66
  let scheduler;
55
67
  let auto = null;
68
+ let activeCtx;
69
+ /** Set the autonomous-loop state and broadcast the active flag on the bus. */
70
+ function setAuto(next) {
71
+ const was = auto !== null;
72
+ auto = next;
73
+ if (was !== (next !== null))
74
+ pi.events.emit(LOOP_AUTO_CHANGED, { active: next !== null });
75
+ }
76
+ // Another extension (e.g. ask_options) hit a decision it cannot safely make
77
+ // while unattended. Stop iterating and let the model report the blocker.
78
+ pi.events.on(LOOP_HALT, (data) => {
79
+ if (!auto)
80
+ return;
81
+ const reason = data?.reason?.trim() || "a decision that needs the user.";
82
+ setAuto(null);
83
+ activeCtx?.ui.notify(`Autonomous loop halted: ${reason}`, "warning");
84
+ });
56
85
  pi.on("session_start", (_event, ctx) => {
86
+ activeCtx = ctx;
57
87
  if (scheduler)
58
88
  return;
59
89
  const isIdle = () => {
@@ -76,7 +106,7 @@ export function setupLoop(pi) {
76
106
  });
77
107
  pi.on("session_shutdown", () => {
78
108
  scheduler?.stop();
79
- auto = null;
109
+ setAuto(null);
80
110
  });
81
111
  // Autonomous continuation: re-prompt on each agent_end until LOOP_DONE or budget.
82
112
  pi.on("agent_end", (event, ctx) => {
@@ -87,12 +117,12 @@ export function setupLoop(pi) {
87
117
  const last = [...event.messages].reverse().find((m) => m.role === "assistant");
88
118
  const text = last ? assistantText(last) : "";
89
119
  if (text.includes(AUTO_LOOP_DONE_TOKEN)) {
90
- auto = null;
120
+ setAuto(null);
91
121
  ctx.ui.notify("Autonomous loop complete.", "info");
92
122
  return;
93
123
  }
94
124
  if (auto.remaining <= 0) {
95
- auto = null;
125
+ setAuto(null);
96
126
  ctx.ui.notify("Autonomous loop stopped: max turns reached.", "warning");
97
127
  return;
98
128
  }
@@ -173,7 +203,7 @@ export function setupLoop(pi) {
173
203
  if (trimmed === "stop") {
174
204
  const had = scheduler.list().length > 0 || auto !== null;
175
205
  scheduler.clear();
176
- auto = null;
206
+ setAuto(null);
177
207
  ctx.ui.notify(had ? "Stopped all loops and scheduled tasks." : "Nothing to stop.", "info");
178
208
  return;
179
209
  }
@@ -198,7 +228,7 @@ export function setupLoop(pi) {
198
228
  ctx.ui.notify("Usage: /loop auto [--max-turns N] <task>", "warning");
199
229
  return;
200
230
  }
201
- auto = { remaining: maxTurns };
231
+ setAuto({ remaining: maxTurns });
202
232
  pi.sendUserMessage(`${rest}\n\n(Autonomous loop: keep working until the task is fully complete, then reply with ${AUTO_LOOP_DONE_TOKEN}.)`, { deliverAs: "followUp" });
203
233
  ctx.ui.notify(`Autonomous loop started (max ${maxTurns} turns). Stop with /loop stop.`, "info");
204
234
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"loop.js","sourceRoot":"","sources":["../../../src/extensions/core/loop.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * /loop — cron scheduler, Cron* tools, and autonomous continuation.\n *\n * `/loop` schedules prompts via cron and drives autonomous continuation. The same\n * scheduler backs the agent-callable CronCreate/CronList/CronDelete tools.\n *\n * /loop \"<cron>\" <prompt> schedule recurring (5-field cron, local time)\n * /loop <5m|2h|1d> <prompt> schedule recurring at a simple interval\n * /loop once \"<cron>\" <prompt> schedule a one-shot\n * /loop list | /loop delete <id> | /loop stop\n * /loop auto [--max-turns N] <task> keep iterating until the task says LOOP_DONE\n */\n\nimport { join } from \"node:path\";\nimport { Type } from \"typebox\";\nimport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tSessionStartEvent,\n} from \"../../core/extensions/types.js\";\nimport { defineTool } from \"../../core/extensions/types.js\";\nimport { TaskScheduler } from \"../../core/scheduler.js\";\n\nconst AUTO_LOOP_DONE_TOKEN = \"LOOP_DONE\";\nconst DEFAULT_AUTO_MAX_TURNS = 10;\n\n/** Convert a simple interval token (\"5m\", \"2h\", \"1d\") to a 5-field cron, or null. */\nfunction intervalToCron(token: string): string | null {\n\tconst m = /^(\\d+)(m|h|d)$/.exec(token.trim());\n\tif (!m) return null;\n\tconst n = Number(m[1]);\n\tif (n < 1) return null;\n\tif (m[2] === \"m\") return `*/${n} * * * *`;\n\tif (m[2] === \"h\") return `0 */${n} * * *`;\n\treturn `0 0 */${n} * *`; // days\n}\n\n/** Pull a quoted cron expression off the front of an argument string. */\nfunction extractQuotedCron(args: string): { cron: string; rest: string } | null {\n\tconst m = /^\"([^\"]+)\"\\s*(.*)$/.exec(args.trim());\n\treturn m ? { cron: m[1].trim(), rest: m[2].trim() } : null;\n}\n\nfunction isFiveFieldCron(expr: string): boolean {\n\treturn expr.trim().split(/\\s+/).length === 5;\n}\n\n/** Flatten an assistant message's text blocks. */\nfunction assistantText(message: { content: unknown }): string {\n\tconst content = message.content;\n\tif (typeof content === \"string\") return content;\n\tif (!Array.isArray(content)) return \"\";\n\treturn content\n\t\t.filter((b): b is { type: \"text\"; text: string } => !!b && (b as { type?: string }).type === \"text\")\n\t\t.map((b) => b.text)\n\t\t.join(\"\\n\");\n}\n\nexport function setupLoop(pi: ExtensionAPI): void {\n\tlet scheduler: TaskScheduler | undefined;\n\tlet auto: { remaining: number } | null = null;\n\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tif (scheduler) return;\n\t\tconst isIdle = () => {\n\t\t\ttry {\n\t\t\t\treturn ctx.isIdle();\n\t\t\t} catch {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t};\n\t\tscheduler = new TaskScheduler({\n\t\t\t// `.agents/` is the primary, cross-vendor home; the legacy `.hoocode/`\n\t\t\t// store is read once and migrates forward on the next persist.\n\t\t\tstorePath: join(ctx.cwd, \".agents\", \"scheduled_tasks.json\"),\n\t\t\tlegacyStorePath: join(ctx.cwd, \".hoocode\", \"scheduled_tasks.json\"),\n\t\t\tfire: (prompt) => pi.sendUserMessage(prompt, { deliverAs: \"followUp\" }),\n\t\t\tisIdle,\n\t\t});\n\t\tscheduler.start();\n\t});\n\n\tpi.on(\"session_shutdown\", () => {\n\t\tscheduler?.stop();\n\t\tauto = null;\n\t});\n\n\t// Autonomous continuation: re-prompt on each agent_end until LOOP_DONE or budget.\n\tpi.on(\"agent_end\", (event, ctx) => {\n\t\tif (!auto) return;\n\t\tif (ctx.hasPendingMessages()) return; // user is steering — yield\n\t\tconst last = [...event.messages].reverse().find((m) => m.role === \"assistant\");\n\t\tconst text = last ? assistantText(last) : \"\";\n\t\tif (text.includes(AUTO_LOOP_DONE_TOKEN)) {\n\t\t\tauto = null;\n\t\t\tctx.ui.notify(\"Autonomous loop complete.\", \"info\");\n\t\t\treturn;\n\t\t}\n\t\tif (auto.remaining <= 0) {\n\t\t\tauto = null;\n\t\t\tctx.ui.notify(\"Autonomous loop stopped: max turns reached.\", \"warning\");\n\t\t\treturn;\n\t\t}\n\t\tauto.remaining -= 1;\n\t\tpi.sendUserMessage(`Continue working toward the goal. Reply with ${AUTO_LOOP_DONE_TOKEN} when fully complete.`, {\n\t\t\tdeliverAs: \"followUp\",\n\t\t});\n\t});\n\n\t// ── Cron* tools (agent-callable) ──────────────────────────────────────────\n\tconst toolText = (s: string) => ({ content: [{ type: \"text\" as const, text: s }], details: undefined });\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronCreate\",\n\t\t\tlabel: \"Schedule Task\",\n\t\t\tdescription:\n\t\t\t\t\"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.\",\n\t\t\tparameters: Type.Object({\n\t\t\t\tcron: Type.String({ description: \"5-field cron expression in local time\" }),\n\t\t\t\tprompt: Type.String({ description: \"Prompt to enqueue at each fire time\" }),\n\t\t\t\trecurring: Type.Optional(Type.Boolean({ description: \"Fire repeatedly (default true) or once\" })),\n\t\t\t}),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tif (!scheduler) return toolText(\"Scheduler not ready.\");\n\t\t\t\tif (!isFiveFieldCron(params.cron)) return toolText(`Invalid cron \"${params.cron}\" (need 5 fields).`);\n\t\t\t\tconst task = scheduler.create({\n\t\t\t\t\tcron: params.cron,\n\t\t\t\t\tprompt: params.prompt,\n\t\t\t\t\trecurring: params.recurring ?? true,\n\t\t\t\t});\n\t\t\t\treturn toolText(`Scheduled ${task.id}: \"${task.cron}\" (${task.recurring ? \"recurring\" : \"once\"})`);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronList\",\n\t\t\tlabel: \"List Scheduled Tasks\",\n\t\t\tdescription: \"List all scheduled tasks (id, cron, recurring, prompt).\",\n\t\t\tparameters: Type.Object({}),\n\t\t\tasync execute() {\n\t\t\t\tconst tasks = scheduler?.list() ?? [];\n\t\t\t\tif (tasks.length === 0) return toolText(\"No scheduled tasks.\");\n\t\t\t\treturn toolText(\n\t\t\t\t\ttasks\n\t\t\t\t\t\t.map((t) => `${t.id} ${t.cron} ${t.recurring ? \"recurring\" : \"once\"} ${JSON.stringify(t.prompt)}`)\n\t\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronDelete\",\n\t\t\tlabel: \"Delete Scheduled Task\",\n\t\t\tdescription: \"Delete a scheduled task by id.\",\n\t\t\tparameters: Type.Object({ id: Type.String({ description: \"Task id from CronCreate/CronList\" }) }),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tconst removed = scheduler?.delete(params.id) ?? false;\n\t\t\t\treturn toolText(removed ? `Deleted ${params.id}.` : `No task ${params.id}.`);\n\t\t\t},\n\t\t}),\n\t);\n\n\t// ── /loop command ─────────────────────────────────────────────────────────\n\tpi.registerCommand(\"loop\", {\n\t\tdescription:\n\t\t\t'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>',\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\t[\"list\", \"delete\", \"stop\", \"once\", \"auto\"]\n\t\t\t\t.filter((s) => s.startsWith(prefix))\n\t\t\t\t.map((s) => ({ value: s, label: s })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst trimmed = args.trim();\n\t\t\tif (!scheduler) {\n\t\t\t\tctx.ui.notify(\"Scheduler not ready yet.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!trimmed || trimmed === \"list\") {\n\t\t\t\tconst tasks = scheduler.list();\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\ttasks.length === 0\n\t\t\t\t\t\t? auto\n\t\t\t\t\t\t\t? `Autonomous loop active (${auto.remaining} turns left).`\n\t\t\t\t\t\t\t: \"No scheduled tasks.\"\n\t\t\t\t\t\t: tasks.map((t) => `${t.id}: ${t.cron} ${t.recurring ? \"\" : \"(once) \"}— ${t.prompt}`).join(\"\\n\"),\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed === \"stop\") {\n\t\t\t\tconst had = scheduler.list().length > 0 || auto !== null;\n\t\t\t\tscheduler.clear();\n\t\t\t\tauto = null;\n\t\t\t\tctx.ui.notify(had ? \"Stopped all loops and scheduled tasks.\" : \"Nothing to stop.\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"delete\")) {\n\t\t\t\tconst id = trimmed.slice(\"delete\".length).trim();\n\t\t\t\tif (!id) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop delete <id>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tctx.ui.notify(scheduler.delete(id) ? `Deleted ${id}.` : `No task ${id}.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"auto\")) {\n\t\t\t\tlet rest = trimmed.slice(\"auto\".length).trim();\n\t\t\t\tlet maxTurns = DEFAULT_AUTO_MAX_TURNS;\n\t\t\t\tconst flag = /^--max-turns\\s+(\\d+)\\s*(.*)$/.exec(rest);\n\t\t\t\tif (flag) {\n\t\t\t\t\tmaxTurns = Number(flag[1]);\n\t\t\t\t\trest = flag[2].trim();\n\t\t\t\t}\n\t\t\t\tif (!rest) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop auto [--max-turns N] <task>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tauto = { remaining: maxTurns };\n\t\t\t\tpi.sendUserMessage(\n\t\t\t\t\t`${rest}\\n\\n(Autonomous loop: keep working until the task is fully complete, then reply with ${AUTO_LOOP_DONE_TOKEN}.)`,\n\t\t\t\t\t{ deliverAs: \"followUp\" },\n\t\t\t\t);\n\t\t\t\tctx.ui.notify(`Autonomous loop started (max ${maxTurns} turns). Stop with /loop stop.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Scheduling: one-shot or recurring, via quoted cron or interval token.\n\t\t\tlet recurring = true;\n\t\t\tlet body = trimmed;\n\t\t\tif (body.startsWith(\"once\")) {\n\t\t\t\trecurring = false;\n\t\t\t\tbody = body.slice(\"once\".length).trim();\n\t\t\t}\n\n\t\t\tlet cron: string | null = null;\n\t\t\tlet prompt = \"\";\n\t\t\tconst quoted = extractQuotedCron(body);\n\t\t\tif (quoted) {\n\t\t\t\tcron = quoted.cron;\n\t\t\t\tprompt = quoted.rest;\n\t\t\t} else {\n\t\t\t\tconst [first, ...restWords] = body.split(/\\s+/);\n\t\t\t\tcron = intervalToCron(first);\n\t\t\t\tprompt = restWords.join(\" \").trim();\n\t\t\t}\n\n\t\t\tif (!cron || !isFiveFieldCron(cron)) {\n\t\t\t\tctx.ui.notify('Usage: /loop \"<cron>\" <prompt> or /loop <5m|2h|1d> <prompt>', \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!prompt) {\n\t\t\t\tctx.ui.notify(\"Nothing to schedule — provide a prompt after the schedule.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst task = scheduler.create({ cron, prompt, recurring });\n\t\t\tctx.ui.notify(\n\t\t\t\t`Scheduled ${task.id}: \"${cron}\" ${recurring ? \"recurring\" : \"once\"} — \"${prompt}\". Manage with /loop list • /loop delete ${task.id}.`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
1
+ {"version":3,"file":"loop.js","sourceRoot":"","sources":["../../../src/extensions/core/loop.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * /loop — cron scheduler, Cron* tools, and autonomous continuation.\n *\n * `/loop` schedules prompts via cron and drives autonomous continuation. The same\n * scheduler backs the agent-callable CronCreate/CronList/CronDelete tools.\n *\n * /loop \"<cron>\" <prompt> schedule recurring (5-field cron, local time)\n * /loop <5m|2h|1d> <prompt> schedule recurring at a simple interval\n * /loop once \"<cron>\" <prompt> schedule a one-shot\n * /loop list | /loop delete <id> | /loop stop\n * /loop auto [--max-turns N] <task> keep iterating until the task says LOOP_DONE\n */\n\nimport { join } from \"node:path\";\nimport { Type } from \"typebox\";\nimport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tSessionStartEvent,\n} from \"../../core/extensions/types.js\";\nimport { defineTool } from \"../../core/extensions/types.js\";\nimport { TaskScheduler } from \"../../core/scheduler.js\";\n\nconst AUTO_LOOP_DONE_TOKEN = \"LOOP_DONE\";\nconst DEFAULT_AUTO_MAX_TURNS = 10;\n\n/**\n * Event-bus channel: the autonomous-loop active state changed.\n * Payload: `{ active: boolean }`. Emitted whenever `/loop auto` starts or stops\n * so other extensions (e.g. ask_options) can adapt to running unattended.\n */\nexport const LOOP_AUTO_CHANGED = \"loop:auto-changed\";\n\n/**\n * Event-bus channel: request to halt the autonomous loop.\n * Payload: `{ reason: string }`. Sent by another extension when it hits a\n * blocker that requires a human decision the loop cannot safely make on its own.\n */\nexport const LOOP_HALT = \"loop:halt\";\n\n/** Convert a simple interval token (\"5m\", \"2h\", \"1d\") to a 5-field cron, or null. */\nfunction intervalToCron(token: string): string | null {\n\tconst m = /^(\\d+)(m|h|d)$/.exec(token.trim());\n\tif (!m) return null;\n\tconst n = Number(m[1]);\n\tif (n < 1) return null;\n\tif (m[2] === \"m\") return `*/${n} * * * *`;\n\tif (m[2] === \"h\") return `0 */${n} * * *`;\n\treturn `0 0 */${n} * *`; // days\n}\n\n/** Pull a quoted cron expression off the front of an argument string. */\nfunction extractQuotedCron(args: string): { cron: string; rest: string } | null {\n\tconst m = /^\"([^\"]+)\"\\s*(.*)$/.exec(args.trim());\n\treturn m ? { cron: m[1].trim(), rest: m[2].trim() } : null;\n}\n\nfunction isFiveFieldCron(expr: string): boolean {\n\treturn expr.trim().split(/\\s+/).length === 5;\n}\n\n/** Flatten an assistant message's text blocks. */\nfunction assistantText(message: { content: unknown }): string {\n\tconst content = message.content;\n\tif (typeof content === \"string\") return content;\n\tif (!Array.isArray(content)) return \"\";\n\treturn content\n\t\t.filter((b): b is { type: \"text\"; text: string } => !!b && (b as { type?: string }).type === \"text\")\n\t\t.map((b) => b.text)\n\t\t.join(\"\\n\");\n}\n\nexport function setupLoop(pi: ExtensionAPI): void {\n\tlet scheduler: TaskScheduler | undefined;\n\tlet auto: { remaining: number } | null = null;\n\tlet activeCtx: ExtensionContext | undefined;\n\n\t/** Set the autonomous-loop state and broadcast the active flag on the bus. */\n\tfunction setAuto(next: { remaining: number } | null): void {\n\t\tconst was = auto !== null;\n\t\tauto = next;\n\t\tif (was !== (next !== null)) pi.events.emit(LOOP_AUTO_CHANGED, { active: next !== null });\n\t}\n\n\t// Another extension (e.g. ask_options) hit a decision it cannot safely make\n\t// while unattended. Stop iterating and let the model report the blocker.\n\tpi.events.on(LOOP_HALT, (data) => {\n\t\tif (!auto) return;\n\t\tconst reason = (data as { reason?: string })?.reason?.trim() || \"a decision that needs the user.\";\n\t\tsetAuto(null);\n\t\tactiveCtx?.ui.notify(`Autonomous loop halted: ${reason}`, \"warning\");\n\t});\n\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tactiveCtx = ctx;\n\t\tif (scheduler) return;\n\t\tconst isIdle = () => {\n\t\t\ttry {\n\t\t\t\treturn ctx.isIdle();\n\t\t\t} catch {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t};\n\t\tscheduler = new TaskScheduler({\n\t\t\t// `.agents/` is the primary, cross-vendor home; the legacy `.hoocode/`\n\t\t\t// store is read once and migrates forward on the next persist.\n\t\t\tstorePath: join(ctx.cwd, \".agents\", \"scheduled_tasks.json\"),\n\t\t\tlegacyStorePath: join(ctx.cwd, \".hoocode\", \"scheduled_tasks.json\"),\n\t\t\tfire: (prompt) => pi.sendUserMessage(prompt, { deliverAs: \"followUp\" }),\n\t\t\tisIdle,\n\t\t});\n\t\tscheduler.start();\n\t});\n\n\tpi.on(\"session_shutdown\", () => {\n\t\tscheduler?.stop();\n\t\tsetAuto(null);\n\t});\n\n\t// Autonomous continuation: re-prompt on each agent_end until LOOP_DONE or budget.\n\tpi.on(\"agent_end\", (event, ctx) => {\n\t\tif (!auto) return;\n\t\tif (ctx.hasPendingMessages()) return; // user is steering — yield\n\t\tconst last = [...event.messages].reverse().find((m) => m.role === \"assistant\");\n\t\tconst text = last ? assistantText(last) : \"\";\n\t\tif (text.includes(AUTO_LOOP_DONE_TOKEN)) {\n\t\t\tsetAuto(null);\n\t\t\tctx.ui.notify(\"Autonomous loop complete.\", \"info\");\n\t\t\treturn;\n\t\t}\n\t\tif (auto.remaining <= 0) {\n\t\t\tsetAuto(null);\n\t\t\tctx.ui.notify(\"Autonomous loop stopped: max turns reached.\", \"warning\");\n\t\t\treturn;\n\t\t}\n\t\tauto.remaining -= 1;\n\t\tpi.sendUserMessage(`Continue working toward the goal. Reply with ${AUTO_LOOP_DONE_TOKEN} when fully complete.`, {\n\t\t\tdeliverAs: \"followUp\",\n\t\t});\n\t});\n\n\t// ── Cron* tools (agent-callable) ──────────────────────────────────────────\n\tconst toolText = (s: string) => ({ content: [{ type: \"text\" as const, text: s }], details: undefined });\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronCreate\",\n\t\t\tlabel: \"Schedule Task\",\n\t\t\tdescription:\n\t\t\t\t\"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.\",\n\t\t\tparameters: Type.Object({\n\t\t\t\tcron: Type.String({ description: \"5-field cron expression in local time\" }),\n\t\t\t\tprompt: Type.String({ description: \"Prompt to enqueue at each fire time\" }),\n\t\t\t\trecurring: Type.Optional(Type.Boolean({ description: \"Fire repeatedly (default true) or once\" })),\n\t\t\t}),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tif (!scheduler) return toolText(\"Scheduler not ready.\");\n\t\t\t\tif (!isFiveFieldCron(params.cron)) return toolText(`Invalid cron \"${params.cron}\" (need 5 fields).`);\n\t\t\t\tconst task = scheduler.create({\n\t\t\t\t\tcron: params.cron,\n\t\t\t\t\tprompt: params.prompt,\n\t\t\t\t\trecurring: params.recurring ?? true,\n\t\t\t\t});\n\t\t\t\treturn toolText(`Scheduled ${task.id}: \"${task.cron}\" (${task.recurring ? \"recurring\" : \"once\"})`);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronList\",\n\t\t\tlabel: \"List Scheduled Tasks\",\n\t\t\tdescription: \"List all scheduled tasks (id, cron, recurring, prompt).\",\n\t\t\tparameters: Type.Object({}),\n\t\t\tasync execute() {\n\t\t\t\tconst tasks = scheduler?.list() ?? [];\n\t\t\t\tif (tasks.length === 0) return toolText(\"No scheduled tasks.\");\n\t\t\t\treturn toolText(\n\t\t\t\t\ttasks\n\t\t\t\t\t\t.map((t) => `${t.id} ${t.cron} ${t.recurring ? \"recurring\" : \"once\"} ${JSON.stringify(t.prompt)}`)\n\t\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronDelete\",\n\t\t\tlabel: \"Delete Scheduled Task\",\n\t\t\tdescription: \"Delete a scheduled task by id.\",\n\t\t\tparameters: Type.Object({ id: Type.String({ description: \"Task id from CronCreate/CronList\" }) }),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tconst removed = scheduler?.delete(params.id) ?? false;\n\t\t\t\treturn toolText(removed ? `Deleted ${params.id}.` : `No task ${params.id}.`);\n\t\t\t},\n\t\t}),\n\t);\n\n\t// ── /loop command ─────────────────────────────────────────────────────────\n\tpi.registerCommand(\"loop\", {\n\t\tdescription:\n\t\t\t'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>',\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\t[\"list\", \"delete\", \"stop\", \"once\", \"auto\"]\n\t\t\t\t.filter((s) => s.startsWith(prefix))\n\t\t\t\t.map((s) => ({ value: s, label: s })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst trimmed = args.trim();\n\t\t\tif (!scheduler) {\n\t\t\t\tctx.ui.notify(\"Scheduler not ready yet.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!trimmed || trimmed === \"list\") {\n\t\t\t\tconst tasks = scheduler.list();\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\ttasks.length === 0\n\t\t\t\t\t\t? auto\n\t\t\t\t\t\t\t? `Autonomous loop active (${auto.remaining} turns left).`\n\t\t\t\t\t\t\t: \"No scheduled tasks.\"\n\t\t\t\t\t\t: tasks.map((t) => `${t.id}: ${t.cron} ${t.recurring ? \"\" : \"(once) \"}— ${t.prompt}`).join(\"\\n\"),\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed === \"stop\") {\n\t\t\t\tconst had = scheduler.list().length > 0 || auto !== null;\n\t\t\t\tscheduler.clear();\n\t\t\t\tsetAuto(null);\n\t\t\t\tctx.ui.notify(had ? \"Stopped all loops and scheduled tasks.\" : \"Nothing to stop.\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"delete\")) {\n\t\t\t\tconst id = trimmed.slice(\"delete\".length).trim();\n\t\t\t\tif (!id) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop delete <id>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tctx.ui.notify(scheduler.delete(id) ? `Deleted ${id}.` : `No task ${id}.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"auto\")) {\n\t\t\t\tlet rest = trimmed.slice(\"auto\".length).trim();\n\t\t\t\tlet maxTurns = DEFAULT_AUTO_MAX_TURNS;\n\t\t\t\tconst flag = /^--max-turns\\s+(\\d+)\\s*(.*)$/.exec(rest);\n\t\t\t\tif (flag) {\n\t\t\t\t\tmaxTurns = Number(flag[1]);\n\t\t\t\t\trest = flag[2].trim();\n\t\t\t\t}\n\t\t\t\tif (!rest) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop auto [--max-turns N] <task>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsetAuto({ remaining: maxTurns });\n\t\t\t\tpi.sendUserMessage(\n\t\t\t\t\t`${rest}\\n\\n(Autonomous loop: keep working until the task is fully complete, then reply with ${AUTO_LOOP_DONE_TOKEN}.)`,\n\t\t\t\t\t{ deliverAs: \"followUp\" },\n\t\t\t\t);\n\t\t\t\tctx.ui.notify(`Autonomous loop started (max ${maxTurns} turns). Stop with /loop stop.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Scheduling: one-shot or recurring, via quoted cron or interval token.\n\t\t\tlet recurring = true;\n\t\t\tlet body = trimmed;\n\t\t\tif (body.startsWith(\"once\")) {\n\t\t\t\trecurring = false;\n\t\t\t\tbody = body.slice(\"once\".length).trim();\n\t\t\t}\n\n\t\t\tlet cron: string | null = null;\n\t\t\tlet prompt = \"\";\n\t\t\tconst quoted = extractQuotedCron(body);\n\t\t\tif (quoted) {\n\t\t\t\tcron = quoted.cron;\n\t\t\t\tprompt = quoted.rest;\n\t\t\t} else {\n\t\t\t\tconst [first, ...restWords] = body.split(/\\s+/);\n\t\t\t\tcron = intervalToCron(first);\n\t\t\t\tprompt = restWords.join(\" \").trim();\n\t\t\t}\n\n\t\t\tif (!cron || !isFiveFieldCron(cron)) {\n\t\t\t\tctx.ui.notify('Usage: /loop \"<cron>\" <prompt> or /loop <5m|2h|1d> <prompt>', \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!prompt) {\n\t\t\t\tctx.ui.notify(\"Nothing to schedule — provide a prompt after the schedule.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst task = scheduler.create({ cron, prompt, recurring });\n\t\t\tctx.ui.notify(\n\t\t\t\t`Scheduled ${task.id}: \"${cron}\" ${recurring ? \"recurring\" : \"once\"} — \"${prompt}\". Manage with /loop list • /loop delete ${task.id}.`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-custom-provider-anthropic",
3
3
  "private": true,
4
- "version": "0.2.128",
4
+ "version": "0.2.129",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-custom-provider-gitlab-duo",
3
3
  "private": true,
4
- "version": "0.2.128",
4
+ "version": "0.2.129",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-sandbox",
3
3
  "private": true,
4
- "version": "0.2.128",
4
+ "version": "0.2.129",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-with-deps",
3
3
  "private": true,
4
- "version": "0.2.128",
4
+ "version": "0.2.129",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-agent",
3
- "version": "0.4.131",
3
+ "version": "0.4.132",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "hoocodeConfig": {
@@ -45,9 +45,9 @@
45
45
  "prepublishOnly": "npm run clean && npm run build"
46
46
  },
47
47
  "dependencies": {
48
- "@kolisachint/hoocode-agent-core": "^0.4.131",
49
- "@kolisachint/hoocode-ai": "^0.4.131",
50
- "@kolisachint/hoocode-tui": "^0.4.131",
48
+ "@kolisachint/hoocode-agent-core": "^0.4.132",
49
+ "@kolisachint/hoocode-ai": "^0.4.132",
50
+ "@kolisachint/hoocode-tui": "^0.4.132",
51
51
  "@silvia-odwyer/photon-node": "^0.3.4",
52
52
  "chalk": "^5.5.0",
53
53
  "cli-highlight": "^2.1.11",