@hasna/loops 0.3.56 → 0.3.58

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.
@@ -461,6 +461,9 @@ function validateTarget(value, label, opts) {
461
461
  }
462
462
  if (value.model !== undefined)
463
463
  assertString(value.model, `${label}.model`);
464
+ if (value.provider === "opencode" && (typeof value.model !== "string" || value.model.trim() === "")) {
465
+ throw new Error(`${label}.model is required for provider opencode; pass a provider/model id such as openrouter/google/gemini-2.5-flash`);
466
+ }
464
467
  if (value.variant !== undefined)
465
468
  assertString(value.variant, `${label}.variant`);
466
469
  if (value.agent !== undefined)
@@ -1257,12 +1260,21 @@ class Store {
1257
1260
  if (!row)
1258
1261
  throw new Error("daemon lease lost");
1259
1262
  }
1263
+ assertNoNestedWorkflowGoal(target, goal) {
1264
+ if (!goal || target.type !== "workflow")
1265
+ return;
1266
+ const workflow = this.getWorkflow(target.workflowId);
1267
+ if (workflow?.goal) {
1268
+ throw new Error(`workflow loop cannot define a loop-level goal when workflow ${workflow.name} already has a top-level goal; remove one goal wrapper`);
1269
+ }
1270
+ }
1260
1271
  createLoop(input, from = new Date) {
1261
1272
  const now = nowIso();
1262
1273
  const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
1263
1274
  name: "loop-target-validation",
1264
1275
  steps: [{ id: "target", target: input.target }]
1265
1276
  }).steps[0].target;
1277
+ this.assertNoNestedWorkflowGoal(target, input.goal);
1266
1278
  const loop = {
1267
1279
  id: genId(),
1268
1280
  name: input.name,
@@ -1434,6 +1446,9 @@ class Store {
1434
1446
  if (this.hasRunningRun(current.id))
1435
1447
  throw new Error(`refusing to retarget running loop: ${current.id}`);
1436
1448
  const workflow = this.requireWorkflow(workflowId);
1449
+ if (current.goal && workflow.goal) {
1450
+ throw new Error(`workflow loop cannot retarget ${current.name} to workflow ${workflow.name} because both define top-level goals`);
1451
+ }
1437
1452
  const target = { ...current.target, workflowId: workflow.id };
1438
1453
  if (opts.workflowTimeoutMs !== undefined)
1439
1454
  target.timeoutMs = opts.workflowTimeoutMs;
@@ -1472,6 +1487,9 @@ class Store {
1472
1487
  throw new Error(`loop is not a workflow loop: ${idOrName}`);
1473
1488
  if (this.hasRunningRun(current.id))
1474
1489
  throw new Error(`refusing to retarget running loop: ${current.id}`);
1490
+ if (current.goal && normalized.goal) {
1491
+ throw new Error(`workflow loop cannot retarget ${current.name} to a workflow that also defines a top-level goal`);
1492
+ }
1475
1493
  const previousWorkflow = this.requireWorkflow(current.target.workflowId);
1476
1494
  const workflow = {
1477
1495
  id: genId(),
@@ -1763,7 +1781,7 @@ class Store {
1763
1781
  if (!sourceDedupeKey)
1764
1782
  throw new Error("cannot refresh workflow invocation without sourceRef.dedupeKey");
1765
1783
  const now = nowIso();
1766
- const claimableStatuses = ["queued", "deferred", "failed", "dead_letter", "cancelled"];
1784
+ const claimableStatuses = ["queued", "deferred"];
1767
1785
  const statusBindings = Object.fromEntries(claimableStatuses.map((status, index) => [`$status${index}`, status]));
1768
1786
  const placeholders = claimableStatuses.map((_, index) => `$status${index}`).join(",");
1769
1787
  const result = this.db.query(`UPDATE workflow_invocations
@@ -1844,28 +1862,35 @@ class Store {
1844
1862
  project_group=excluded.project_group,
1845
1863
  priority=excluded.priority,
1846
1864
  status=CASE
1847
- WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running')
1865
+ WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled')
1848
1866
  THEN workflow_work_items.status
1849
1867
  ELSE excluded.status
1850
1868
  END,
1851
1869
  workflow_id=CASE
1852
- WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.workflow_id
1870
+ WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.workflow_id
1853
1871
  ELSE NULL
1854
1872
  END,
1855
1873
  loop_id=CASE
1856
- WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.loop_id
1874
+ WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.loop_id
1857
1875
  ELSE NULL
1858
1876
  END,
1859
1877
  workflow_run_id=CASE
1860
- WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.workflow_run_id
1878
+ WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.workflow_run_id
1861
1879
  ELSE NULL
1862
1880
  END,
1863
1881
  lease_expires_at=CASE
1864
- WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.lease_expires_at
1882
+ WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.lease_expires_at
1865
1883
  ELSE NULL
1866
1884
  END,
1867
1885
  next_attempt_at=excluded.next_attempt_at,
1868
- last_reason=COALESCE(excluded.last_reason, workflow_work_items.last_reason),
1886
+ last_reason=CASE
1887
+ WHEN workflow_work_items.attempts > 0
1888
+ AND workflow_work_items.status IN ('queued', 'deferred')
1889
+ AND workflow_work_items.last_reason IS NOT NULL
1890
+ AND excluded.last_reason IS NOT NULL
1891
+ THEN workflow_work_items.last_reason || '; ' || excluded.last_reason
1892
+ ELSE COALESCE(excluded.last_reason, workflow_work_items.last_reason)
1893
+ END,
1869
1894
  updated_at=excluded.updated_at`).run({
1870
1895
  $id: id,
1871
1896
  $routeKey: input.routeKey,
@@ -1918,11 +1943,39 @@ class Store {
1918
1943
  const projectGroup = args.projectGroup ? this.db.query(`SELECT COUNT(*) AS count FROM workflow_work_items WHERE status IN (${placeholders}) AND project_group = ?`).get(...active, args.projectGroup)?.count ?? 0 : undefined;
1919
1944
  return { global, project, ...projectGroup !== undefined ? { projectGroup } : {} };
1920
1945
  }
1946
+ requeueWorkflowWorkItem(id, patch = {}) {
1947
+ const current = this.getWorkflowWorkItem(id);
1948
+ if (!current)
1949
+ throw new Error(`workflow work item not found: ${id}`);
1950
+ const requeueableStatuses = ["succeeded", "failed", "dead_letter", "cancelled"];
1951
+ if (!requeueableStatuses.includes(current.status)) {
1952
+ throw new Error(`workflow work item is not requeueable: ${id} status=${current.status}`);
1953
+ }
1954
+ const now = nowIso();
1955
+ const reason = patch.reason?.trim() || `requeued from ${current.status}`;
1956
+ const placeholders = requeueableStatuses.map(() => "?").join(",");
1957
+ const res = this.db.query(`UPDATE workflow_work_items
1958
+ SET status='queued', workflow_id=NULL, loop_id=NULL, workflow_run_id=NULL,
1959
+ next_attempt_at=NULL, lease_expires_at=NULL, last_reason=?, updated_at=?
1960
+ WHERE id=? AND status IN (${placeholders})`).run(reason, now, id, ...requeueableStatuses);
1961
+ const item = this.getWorkflowWorkItem(id);
1962
+ if (!item)
1963
+ throw new Error(`workflow work item not found after requeue: ${id}`);
1964
+ if (res.changes !== 1)
1965
+ throw new Error(`workflow work item was not requeued: ${id} status=${item.status}`);
1966
+ return item;
1967
+ }
1921
1968
  admitWorkflowWorkItem(id, patch) {
1922
1969
  const now = nowIso();
1923
1970
  const res = this.db.query(`UPDATE workflow_work_items
1924
1971
  SET status='admitted', attempts=attempts + 1, workflow_id=$workflowId, loop_id=$loopId,
1925
- next_attempt_at=NULL, lease_expires_at=NULL, last_reason=$reason, updated_at=$updated
1972
+ next_attempt_at=NULL,
1973
+ lease_expires_at=NULL,
1974
+ last_reason=CASE
1975
+ WHEN last_reason IS NOT NULL AND $reason IS NOT NULL THEN last_reason || '; ' || $reason
1976
+ ELSE COALESCE($reason, last_reason)
1977
+ END,
1978
+ updated_at=$updated
1926
1979
  WHERE id=$id AND status IN ('queued', 'deferred')`).run({
1927
1980
  $id: id,
1928
1981
  $workflowId: patch.workflowId,
@@ -3464,6 +3517,9 @@ function assertSupportedAgentOptions(target) {
3464
3517
  assertStringOption(target.agent, `${target.provider}.agent`);
3465
3518
  assertStringOption(target.authProfile, `${target.provider}.authProfile`);
3466
3519
  assertStringOption(target.configIsolation, `${target.provider}.configIsolation`);
3520
+ if (target.provider === "opencode" && (target.model === undefined || target.model.trim() === "")) {
3521
+ throw new Error("opencode.model is required; pass a provider/model id such as openrouter/google/gemini-2.5-flash");
3522
+ }
3467
3523
  if (target.configIsolation !== undefined && target.configIsolation !== "safe" && target.configIsolation !== "none") {
3468
3524
  throw new Error(`${target.provider}.configIsolation must be safe or none`);
3469
3525
  }
@@ -4236,6 +4292,78 @@ function sameBlockerKey(values) {
4236
4292
  return values.map((value) => value.trim()).filter(Boolean).join(`
4237
4293
  `) || "goal completion remains unproven";
4238
4294
  }
4295
+ function planStatusForGoal(goal) {
4296
+ if (goal.status === "usageLimited")
4297
+ return "blocked";
4298
+ return goal.status;
4299
+ }
4300
+ function syncReadyFlags(store, goal, nodes, opts) {
4301
+ const withReady = updateReadyFlags(nodes, planStatusForGoal(goal));
4302
+ for (const node of withReady) {
4303
+ const current = nodes.find((entry) => entry.key === node.key);
4304
+ if (current && current.ready !== node.ready) {
4305
+ store.updateGoalPlanNode(goal.goalId, node.key, { ready: node.ready }, { daemonLeaseId: opts.daemonLeaseId });
4306
+ }
4307
+ }
4308
+ return withReady;
4309
+ }
4310
+ function summarizeLabels(values, limit = 5) {
4311
+ if (values.length <= limit)
4312
+ return values.join(", ");
4313
+ return `${values.slice(0, limit).join(", ")} and ${values.length - limit} more`;
4314
+ }
4315
+ function noReadyDiagnostic(goal, nodes) {
4316
+ const planStatus = planStatusForGoal(goal);
4317
+ const byKey = new Map(nodes.map((node) => [node.key, node]));
4318
+ const pendingNodes = nodes.filter((node) => node.status === "pending");
4319
+ const incompleteNodes = nodes.filter((node) => node.status !== "complete");
4320
+ const pendingDetails = pendingNodes.map((node) => ({
4321
+ key: node.key,
4322
+ status: node.status,
4323
+ ready: node.ready,
4324
+ tokenBudget: node.tokenBudget,
4325
+ tokensUsed: node.tokensUsed,
4326
+ budgetExhausted: nodeBudgetExhausted(node),
4327
+ unmetDependencies: node.dependsOn.map((dependency) => ({ key: dependency, status: byKey.get(dependency)?.status ?? "missing" })).filter((dependency) => dependency.status !== "complete")
4328
+ }));
4329
+ const incompleteDetails = incompleteNodes.map((node) => ({
4330
+ key: node.key,
4331
+ status: node.status,
4332
+ ready: node.ready
4333
+ }));
4334
+ let owner = "goal-plan";
4335
+ let cause = "goal plan has no ready nodes";
4336
+ if (planStatus !== "active") {
4337
+ owner = "goal";
4338
+ cause = `goal status is ${goal.status}`;
4339
+ } else if (pendingNodes.length === 0) {
4340
+ owner = "goal-plan";
4341
+ cause = `goal plan has no pending runnable nodes; incomplete nodes: ${summarizeLabels(incompleteDetails.map((node) => `${node.key}:${node.status}`))}`;
4342
+ } else if (pendingDetails.every((node) => node.budgetExhausted)) {
4343
+ owner = "goal-plan-node";
4344
+ cause = `all pending nodes are budget-exhausted: ${summarizeLabels(pendingDetails.map((node) => node.key))}`;
4345
+ } else {
4346
+ const missingDependencies = pendingDetails.flatMap((node) => node.unmetDependencies.filter((dependency) => dependency.status === "missing").map((dependency) => `${node.key}->${dependency.key}`));
4347
+ if (missingDependencies.length > 0) {
4348
+ owner = "goal-plan";
4349
+ cause = `pending nodes reference missing dependencies: ${summarizeLabels(missingDependencies)}`;
4350
+ } else if (pendingDetails.every((node) => node.unmetDependencies.length > 0 || node.budgetExhausted)) {
4351
+ owner = "goal-plan-node";
4352
+ const waiting = pendingDetails.filter((node) => node.unmetDependencies.length > 0).map((node) => `${node.key} waits on ${node.unmetDependencies.map((dependency) => `${dependency.key}:${dependency.status}`).join(",")}`);
4353
+ const exhausted = pendingDetails.filter((node) => node.budgetExhausted).map((node) => `${node.key} budget exhausted`);
4354
+ cause = `pending nodes are blocked by prerequisites: ${summarizeLabels([...waiting, ...exhausted])}`;
4355
+ }
4356
+ }
4357
+ const blocker = `${cause}; owner=${owner}`;
4358
+ return {
4359
+ owner,
4360
+ blocker,
4361
+ planStatus,
4362
+ rollup: rollupSummary(nodes),
4363
+ pendingNodes: pendingDetails,
4364
+ incompleteNodes: incompleteDetails
4365
+ };
4366
+ }
4239
4367
  function metadataFor(goal, node, context) {
4240
4368
  return {
4241
4369
  loopId: context?.loopId,
@@ -4296,13 +4424,14 @@ async function planGoal(store, goal, spec, model, opts) {
4296
4424
  }, { daemonLeaseId: opts.daemonLeaseId });
4297
4425
  return store.createGoalPlanNodes(goal.goalId, rawNodes, { daemonLeaseId: opts.daemonLeaseId });
4298
4426
  }
4299
- function stdoutFor(goal, nodes, evidence, validation) {
4427
+ function stdoutFor(goal, nodes, evidence, validation, diagnostics) {
4300
4428
  return JSON.stringify({
4301
4429
  goal,
4302
4430
  rollup: rollupSummary(nodes),
4303
4431
  nodes,
4304
4432
  evidence,
4305
- validation
4433
+ validation,
4434
+ diagnostics
4306
4435
  }, null, 2);
4307
4436
  }
4308
4437
  async function runGoal(store, input, opts = {}) {
@@ -4335,6 +4464,7 @@ async function runGoal(store, input, opts = {}) {
4335
4464
  let validation;
4336
4465
  let lastBlocker = "";
4337
4466
  let repeatedBlockerCount = 0;
4467
+ let lastDiagnostic;
4338
4468
  if (budgetExhausted(goal)) {
4339
4469
  goal = store.updateGoalStatus(goal.goalId, "budgetLimited", { daemonLeaseId: opts.daemonLeaseId });
4340
4470
  return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence), "goal token budget exhausted after planning", startedAt);
@@ -4345,13 +4475,13 @@ async function runGoal(store, input, opts = {}) {
4345
4475
  return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence), "goal cancelled", startedAt);
4346
4476
  }
4347
4477
  goal = store.requireGoal(goal.goalId);
4348
- nodes = store.listGoalPlanNodes(goal.goalId);
4478
+ nodes = syncReadyFlags(store, goal, store.listGoalPlanNodes(goal.goalId), opts);
4349
4479
  if (budgetExhausted(goal)) {
4350
4480
  goal = store.updateGoalStatus(goal.goalId, "budgetLimited", { daemonLeaseId: opts.daemonLeaseId });
4351
4481
  return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence), "goal token budget exhausted", startedAt);
4352
4482
  }
4353
4483
  const readyKeys = readyNodeKeys({
4354
- status: goal.status === "active" ? "active" : goal.status === "budgetLimited" ? "budgetLimited" : "blocked",
4484
+ status: planStatusForGoal(goal),
4355
4485
  nodes
4356
4486
  });
4357
4487
  if (readyKeys.length > 0) {
@@ -4450,7 +4580,9 @@ ${result.stderr}`);
4450
4580
  }
4451
4581
  continue;
4452
4582
  }
4453
- const blocker = "no ready goal nodes and goal plan is incomplete";
4583
+ const diagnostic = noReadyDiagnostic(goal, nodes);
4584
+ lastDiagnostic = diagnostic;
4585
+ const blocker = diagnostic.blocker;
4454
4586
  if (blocker === lastBlocker)
4455
4587
  repeatedBlockerCount += 1;
4456
4588
  else {
@@ -4462,15 +4594,16 @@ ${result.stderr}`);
4462
4594
  turn,
4463
4595
  phase: "status",
4464
4596
  status: repeatedBlockerCount >= 3 ? "blocked" : "active",
4465
- evidence: { blocker }
4597
+ evidence: diagnostic
4466
4598
  }, { daemonLeaseId: opts.daemonLeaseId });
4467
4599
  if (repeatedBlockerCount >= 3) {
4468
4600
  goal = store.updateGoalStatus(goal.goalId, "blocked", { daemonLeaseId: opts.daemonLeaseId });
4469
- return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence, validation), blocker, startedAt);
4601
+ return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence, validation, diagnostic), blocker, startedAt);
4470
4602
  }
4471
4603
  }
4472
4604
  goal = store.updateGoalStatus(goal.goalId, "usageLimited", { daemonLeaseId: opts.daemonLeaseId });
4473
- return resultFromGoal(goal, "failed", stdoutFor(goal, store.listGoalPlanNodes(goal.goalId), evidence, validation), "goal max turns exhausted", startedAt);
4605
+ const exhaustedError = lastDiagnostic?.blocker ?? (lastBlocker ? `${lastBlocker}; max turns exhausted` : "goal max turns exhausted");
4606
+ return resultFromGoal(goal, "failed", stdoutFor(goal, store.listGoalPlanNodes(goal.goalId), evidence, validation, lastDiagnostic), exhaustedError, startedAt);
4474
4607
  }
4475
4608
 
4476
4609
  // src/lib/workflow-runner.ts
@@ -4499,6 +4632,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
4499
4632
  const workflowWithoutGoal = { ...workflow, goal: undefined };
4500
4633
  return runGoal(store, workflow.goal, {
4501
4634
  ...opts,
4635
+ model: opts.goalModel,
4502
4636
  context: {
4503
4637
  loopId: opts.loop?.id,
4504
4638
  loopName: opts.loop?.name,
@@ -4591,6 +4725,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
4591
4725
  if (step.goal) {
4592
4726
  result = await runGoal(store, step.goal, {
4593
4727
  ...opts,
4728
+ model: opts.goalModel,
4594
4729
  target: targetWithStepAccount(step),
4595
4730
  signal: controller.signal,
4596
4731
  context: {
@@ -4732,6 +4867,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
4732
4867
  if (loop.goal) {
4733
4868
  return runGoal(store, loop.goal, {
4734
4869
  ...opts,
4870
+ model: opts.goalModel,
4735
4871
  target: loop.target,
4736
4872
  context: {
4737
4873
  loopId: loop.id,
@@ -4753,8 +4889,10 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
4753
4889
  }
4754
4890
  }
4755
4891
  if (loop.goal) {
4892
+ const workflowForLoopGoal = workflow.goal ? { ...workflow, goal: undefined } : workflow;
4756
4893
  return runGoal(store, loop.goal, {
4757
4894
  ...opts,
4895
+ model: opts.goalModel,
4758
4896
  context: {
4759
4897
  loopId: loop.id,
4760
4898
  loopName: loop.name,
@@ -4763,7 +4901,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
4763
4901
  workflowId: workflow.id,
4764
4902
  workflowName: workflow.name
4765
4903
  },
4766
- executeNode: async (node) => executeWorkflow(store, workflow, {
4904
+ executeNode: async (node) => executeWorkflow(store, workflowForLoopGoal, {
4767
4905
  ...opts,
4768
4906
  loop,
4769
4907
  loopRun: run,
@@ -5493,14 +5631,15 @@ function enableStartup(result) {
5493
5631
  // package.json
5494
5632
  var package_default = {
5495
5633
  name: "@hasna/loops",
5496
- version: "0.3.56",
5634
+ version: "0.3.58",
5497
5635
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5498
5636
  type: "module",
5499
5637
  main: "dist/index.js",
5500
5638
  types: "dist/index.d.ts",
5501
5639
  bin: {
5502
5640
  loops: "dist/cli/index.js",
5503
- "loops-daemon": "dist/daemon/index.js"
5641
+ "loops-daemon": "dist/daemon/index.js",
5642
+ "loops-mcp": "dist/mcp/index.js"
5504
5643
  },
5505
5644
  exports: {
5506
5645
  ".": {
@@ -5511,6 +5650,10 @@ var package_default = {
5511
5650
  types: "./dist/sdk/index.d.ts",
5512
5651
  import: "./dist/sdk/index.js"
5513
5652
  },
5653
+ "./mcp": {
5654
+ types: "./dist/mcp/index.d.ts",
5655
+ import: "./dist/mcp/index.js"
5656
+ },
5514
5657
  "./storage": {
5515
5658
  types: "./dist/lib/store.d.ts",
5516
5659
  import: "./dist/lib/store.js"
@@ -5523,7 +5666,7 @@ var package_default = {
5523
5666
  "LICENSE"
5524
5667
  ],
5525
5668
  scripts: {
5526
- build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
5669
+ build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
5527
5670
  "build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
5528
5671
  typecheck: "tsc --noEmit",
5529
5672
  test: "bun test",
@@ -5559,6 +5702,7 @@ var package_default = {
5559
5702
  dependencies: {
5560
5703
  "@hasna/events": "^0.1.9",
5561
5704
  "@hasna/machines": "0.0.49",
5705
+ "@modelcontextprotocol/sdk": "^1.29.0",
5562
5706
  "@openrouter/ai-sdk-provider": "2.9.1",
5563
5707
  ai: "6.0.204",
5564
5708
  commander: "^13.1.0",
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export * from "./types.js";
2
2
  export { LoopsClient, loops, openAutomationsRuntimeBinding } from "./sdk/index.js";
3
3
  export type { LoopsClientOptions } from "./sdk/index.js";
4
+ export { createLoopsMcpServer, listToolsForCli, LOOPS_MCP_TOOLS } from "./mcp/index.js";
5
+ export type { LoopsMcpToolMetadata } from "./mcp/index.js";
4
6
  export { Store } from "./lib/store.js";
5
7
  export { parseDuration, parseCron, nextCronRun, initialNextRun, computeNextAfter } from "./lib/schedule.js";
6
8
  export { executeLoop, executeTarget, preflightTarget } from "./lib/executor.js";