@hasna/loops 0.3.56 → 0.3.57
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/README.md +188 -14
- package/dist/cli/index.js +1325 -175
- package/dist/daemon/index.js +143 -19
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4031 -2876
- package/dist/lib/health.d.ts +2 -2
- package/dist/lib/store.d.ts +3 -0
- package/dist/lib/store.js +46 -8
- package/dist/lib/templates.d.ts +4 -0
- package/dist/mcp/index.d.ts +12 -0
- package/dist/mcp/index.js +5821 -0
- package/dist/sdk/index.js +134 -16
- package/docs/AUTOMATION_RUNTIME_DESIGN.md +228 -0
- package/docs/USAGE.md +123 -15
- package/package.json +9 -3
package/dist/daemon/index.js
CHANGED
|
@@ -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)
|
|
@@ -1763,7 +1766,7 @@ class Store {
|
|
|
1763
1766
|
if (!sourceDedupeKey)
|
|
1764
1767
|
throw new Error("cannot refresh workflow invocation without sourceRef.dedupeKey");
|
|
1765
1768
|
const now = nowIso();
|
|
1766
|
-
const claimableStatuses = ["queued", "deferred"
|
|
1769
|
+
const claimableStatuses = ["queued", "deferred"];
|
|
1767
1770
|
const statusBindings = Object.fromEntries(claimableStatuses.map((status, index) => [`$status${index}`, status]));
|
|
1768
1771
|
const placeholders = claimableStatuses.map((_, index) => `$status${index}`).join(",");
|
|
1769
1772
|
const result = this.db.query(`UPDATE workflow_invocations
|
|
@@ -1844,28 +1847,35 @@ class Store {
|
|
|
1844
1847
|
project_group=excluded.project_group,
|
|
1845
1848
|
priority=excluded.priority,
|
|
1846
1849
|
status=CASE
|
|
1847
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running')
|
|
1850
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled')
|
|
1848
1851
|
THEN workflow_work_items.status
|
|
1849
1852
|
ELSE excluded.status
|
|
1850
1853
|
END,
|
|
1851
1854
|
workflow_id=CASE
|
|
1852
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.workflow_id
|
|
1855
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.workflow_id
|
|
1853
1856
|
ELSE NULL
|
|
1854
1857
|
END,
|
|
1855
1858
|
loop_id=CASE
|
|
1856
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.loop_id
|
|
1859
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.loop_id
|
|
1857
1860
|
ELSE NULL
|
|
1858
1861
|
END,
|
|
1859
1862
|
workflow_run_id=CASE
|
|
1860
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.workflow_run_id
|
|
1863
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.workflow_run_id
|
|
1861
1864
|
ELSE NULL
|
|
1862
1865
|
END,
|
|
1863
1866
|
lease_expires_at=CASE
|
|
1864
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.lease_expires_at
|
|
1867
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.lease_expires_at
|
|
1865
1868
|
ELSE NULL
|
|
1866
1869
|
END,
|
|
1867
1870
|
next_attempt_at=excluded.next_attempt_at,
|
|
1868
|
-
last_reason=
|
|
1871
|
+
last_reason=CASE
|
|
1872
|
+
WHEN workflow_work_items.attempts > 0
|
|
1873
|
+
AND workflow_work_items.status IN ('queued', 'deferred')
|
|
1874
|
+
AND workflow_work_items.last_reason IS NOT NULL
|
|
1875
|
+
AND excluded.last_reason IS NOT NULL
|
|
1876
|
+
THEN workflow_work_items.last_reason || '; ' || excluded.last_reason
|
|
1877
|
+
ELSE COALESCE(excluded.last_reason, workflow_work_items.last_reason)
|
|
1878
|
+
END,
|
|
1869
1879
|
updated_at=excluded.updated_at`).run({
|
|
1870
1880
|
$id: id,
|
|
1871
1881
|
$routeKey: input.routeKey,
|
|
@@ -1918,11 +1928,39 @@ class Store {
|
|
|
1918
1928
|
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
1929
|
return { global, project, ...projectGroup !== undefined ? { projectGroup } : {} };
|
|
1920
1930
|
}
|
|
1931
|
+
requeueWorkflowWorkItem(id, patch = {}) {
|
|
1932
|
+
const current = this.getWorkflowWorkItem(id);
|
|
1933
|
+
if (!current)
|
|
1934
|
+
throw new Error(`workflow work item not found: ${id}`);
|
|
1935
|
+
const requeueableStatuses = ["succeeded", "failed", "dead_letter", "cancelled"];
|
|
1936
|
+
if (!requeueableStatuses.includes(current.status)) {
|
|
1937
|
+
throw new Error(`workflow work item is not requeueable: ${id} status=${current.status}`);
|
|
1938
|
+
}
|
|
1939
|
+
const now = nowIso();
|
|
1940
|
+
const reason = patch.reason?.trim() || `requeued from ${current.status}`;
|
|
1941
|
+
const placeholders = requeueableStatuses.map(() => "?").join(",");
|
|
1942
|
+
const res = this.db.query(`UPDATE workflow_work_items
|
|
1943
|
+
SET status='queued', workflow_id=NULL, loop_id=NULL, workflow_run_id=NULL,
|
|
1944
|
+
next_attempt_at=NULL, lease_expires_at=NULL, last_reason=?, updated_at=?
|
|
1945
|
+
WHERE id=? AND status IN (${placeholders})`).run(reason, now, id, ...requeueableStatuses);
|
|
1946
|
+
const item = this.getWorkflowWorkItem(id);
|
|
1947
|
+
if (!item)
|
|
1948
|
+
throw new Error(`workflow work item not found after requeue: ${id}`);
|
|
1949
|
+
if (res.changes !== 1)
|
|
1950
|
+
throw new Error(`workflow work item was not requeued: ${id} status=${item.status}`);
|
|
1951
|
+
return item;
|
|
1952
|
+
}
|
|
1921
1953
|
admitWorkflowWorkItem(id, patch) {
|
|
1922
1954
|
const now = nowIso();
|
|
1923
1955
|
const res = this.db.query(`UPDATE workflow_work_items
|
|
1924
1956
|
SET status='admitted', attempts=attempts + 1, workflow_id=$workflowId, loop_id=$loopId,
|
|
1925
|
-
next_attempt_at=NULL,
|
|
1957
|
+
next_attempt_at=NULL,
|
|
1958
|
+
lease_expires_at=NULL,
|
|
1959
|
+
last_reason=CASE
|
|
1960
|
+
WHEN last_reason IS NOT NULL AND $reason IS NOT NULL THEN last_reason || '; ' || $reason
|
|
1961
|
+
ELSE COALESCE($reason, last_reason)
|
|
1962
|
+
END,
|
|
1963
|
+
updated_at=$updated
|
|
1926
1964
|
WHERE id=$id AND status IN ('queued', 'deferred')`).run({
|
|
1927
1965
|
$id: id,
|
|
1928
1966
|
$workflowId: patch.workflowId,
|
|
@@ -3464,6 +3502,9 @@ function assertSupportedAgentOptions(target) {
|
|
|
3464
3502
|
assertStringOption(target.agent, `${target.provider}.agent`);
|
|
3465
3503
|
assertStringOption(target.authProfile, `${target.provider}.authProfile`);
|
|
3466
3504
|
assertStringOption(target.configIsolation, `${target.provider}.configIsolation`);
|
|
3505
|
+
if (target.provider === "opencode" && (target.model === undefined || target.model.trim() === "")) {
|
|
3506
|
+
throw new Error("opencode.model is required; pass a provider/model id such as openrouter/google/gemini-2.5-flash");
|
|
3507
|
+
}
|
|
3467
3508
|
if (target.configIsolation !== undefined && target.configIsolation !== "safe" && target.configIsolation !== "none") {
|
|
3468
3509
|
throw new Error(`${target.provider}.configIsolation must be safe or none`);
|
|
3469
3510
|
}
|
|
@@ -4236,6 +4277,78 @@ function sameBlockerKey(values) {
|
|
|
4236
4277
|
return values.map((value) => value.trim()).filter(Boolean).join(`
|
|
4237
4278
|
`) || "goal completion remains unproven";
|
|
4238
4279
|
}
|
|
4280
|
+
function planStatusForGoal(goal) {
|
|
4281
|
+
if (goal.status === "usageLimited")
|
|
4282
|
+
return "blocked";
|
|
4283
|
+
return goal.status;
|
|
4284
|
+
}
|
|
4285
|
+
function syncReadyFlags(store, goal, nodes, opts) {
|
|
4286
|
+
const withReady = updateReadyFlags(nodes, planStatusForGoal(goal));
|
|
4287
|
+
for (const node of withReady) {
|
|
4288
|
+
const current = nodes.find((entry) => entry.key === node.key);
|
|
4289
|
+
if (current && current.ready !== node.ready) {
|
|
4290
|
+
store.updateGoalPlanNode(goal.goalId, node.key, { ready: node.ready }, { daemonLeaseId: opts.daemonLeaseId });
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
return withReady;
|
|
4294
|
+
}
|
|
4295
|
+
function summarizeLabels(values, limit = 5) {
|
|
4296
|
+
if (values.length <= limit)
|
|
4297
|
+
return values.join(", ");
|
|
4298
|
+
return `${values.slice(0, limit).join(", ")} and ${values.length - limit} more`;
|
|
4299
|
+
}
|
|
4300
|
+
function noReadyDiagnostic(goal, nodes) {
|
|
4301
|
+
const planStatus = planStatusForGoal(goal);
|
|
4302
|
+
const byKey = new Map(nodes.map((node) => [node.key, node]));
|
|
4303
|
+
const pendingNodes = nodes.filter((node) => node.status === "pending");
|
|
4304
|
+
const incompleteNodes = nodes.filter((node) => node.status !== "complete");
|
|
4305
|
+
const pendingDetails = pendingNodes.map((node) => ({
|
|
4306
|
+
key: node.key,
|
|
4307
|
+
status: node.status,
|
|
4308
|
+
ready: node.ready,
|
|
4309
|
+
tokenBudget: node.tokenBudget,
|
|
4310
|
+
tokensUsed: node.tokensUsed,
|
|
4311
|
+
budgetExhausted: nodeBudgetExhausted(node),
|
|
4312
|
+
unmetDependencies: node.dependsOn.map((dependency) => ({ key: dependency, status: byKey.get(dependency)?.status ?? "missing" })).filter((dependency) => dependency.status !== "complete")
|
|
4313
|
+
}));
|
|
4314
|
+
const incompleteDetails = incompleteNodes.map((node) => ({
|
|
4315
|
+
key: node.key,
|
|
4316
|
+
status: node.status,
|
|
4317
|
+
ready: node.ready
|
|
4318
|
+
}));
|
|
4319
|
+
let owner = "goal-plan";
|
|
4320
|
+
let cause = "goal plan has no ready nodes";
|
|
4321
|
+
if (planStatus !== "active") {
|
|
4322
|
+
owner = "goal";
|
|
4323
|
+
cause = `goal status is ${goal.status}`;
|
|
4324
|
+
} else if (pendingNodes.length === 0) {
|
|
4325
|
+
owner = "goal-plan";
|
|
4326
|
+
cause = `goal plan has no pending runnable nodes; incomplete nodes: ${summarizeLabels(incompleteDetails.map((node) => `${node.key}:${node.status}`))}`;
|
|
4327
|
+
} else if (pendingDetails.every((node) => node.budgetExhausted)) {
|
|
4328
|
+
owner = "goal-plan-node";
|
|
4329
|
+
cause = `all pending nodes are budget-exhausted: ${summarizeLabels(pendingDetails.map((node) => node.key))}`;
|
|
4330
|
+
} else {
|
|
4331
|
+
const missingDependencies = pendingDetails.flatMap((node) => node.unmetDependencies.filter((dependency) => dependency.status === "missing").map((dependency) => `${node.key}->${dependency.key}`));
|
|
4332
|
+
if (missingDependencies.length > 0) {
|
|
4333
|
+
owner = "goal-plan";
|
|
4334
|
+
cause = `pending nodes reference missing dependencies: ${summarizeLabels(missingDependencies)}`;
|
|
4335
|
+
} else if (pendingDetails.every((node) => node.unmetDependencies.length > 0 || node.budgetExhausted)) {
|
|
4336
|
+
owner = "goal-plan-node";
|
|
4337
|
+
const waiting = pendingDetails.filter((node) => node.unmetDependencies.length > 0).map((node) => `${node.key} waits on ${node.unmetDependencies.map((dependency) => `${dependency.key}:${dependency.status}`).join(",")}`);
|
|
4338
|
+
const exhausted = pendingDetails.filter((node) => node.budgetExhausted).map((node) => `${node.key} budget exhausted`);
|
|
4339
|
+
cause = `pending nodes are blocked by prerequisites: ${summarizeLabels([...waiting, ...exhausted])}`;
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
4342
|
+
const blocker = `${cause}; owner=${owner}`;
|
|
4343
|
+
return {
|
|
4344
|
+
owner,
|
|
4345
|
+
blocker,
|
|
4346
|
+
planStatus,
|
|
4347
|
+
rollup: rollupSummary(nodes),
|
|
4348
|
+
pendingNodes: pendingDetails,
|
|
4349
|
+
incompleteNodes: incompleteDetails
|
|
4350
|
+
};
|
|
4351
|
+
}
|
|
4239
4352
|
function metadataFor(goal, node, context) {
|
|
4240
4353
|
return {
|
|
4241
4354
|
loopId: context?.loopId,
|
|
@@ -4296,13 +4409,14 @@ async function planGoal(store, goal, spec, model, opts) {
|
|
|
4296
4409
|
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
4297
4410
|
return store.createGoalPlanNodes(goal.goalId, rawNodes, { daemonLeaseId: opts.daemonLeaseId });
|
|
4298
4411
|
}
|
|
4299
|
-
function stdoutFor(goal, nodes, evidence, validation) {
|
|
4412
|
+
function stdoutFor(goal, nodes, evidence, validation, diagnostics) {
|
|
4300
4413
|
return JSON.stringify({
|
|
4301
4414
|
goal,
|
|
4302
4415
|
rollup: rollupSummary(nodes),
|
|
4303
4416
|
nodes,
|
|
4304
4417
|
evidence,
|
|
4305
|
-
validation
|
|
4418
|
+
validation,
|
|
4419
|
+
diagnostics
|
|
4306
4420
|
}, null, 2);
|
|
4307
4421
|
}
|
|
4308
4422
|
async function runGoal(store, input, opts = {}) {
|
|
@@ -4335,6 +4449,7 @@ async function runGoal(store, input, opts = {}) {
|
|
|
4335
4449
|
let validation;
|
|
4336
4450
|
let lastBlocker = "";
|
|
4337
4451
|
let repeatedBlockerCount = 0;
|
|
4452
|
+
let lastDiagnostic;
|
|
4338
4453
|
if (budgetExhausted(goal)) {
|
|
4339
4454
|
goal = store.updateGoalStatus(goal.goalId, "budgetLimited", { daemonLeaseId: opts.daemonLeaseId });
|
|
4340
4455
|
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence), "goal token budget exhausted after planning", startedAt);
|
|
@@ -4345,13 +4460,13 @@ async function runGoal(store, input, opts = {}) {
|
|
|
4345
4460
|
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence), "goal cancelled", startedAt);
|
|
4346
4461
|
}
|
|
4347
4462
|
goal = store.requireGoal(goal.goalId);
|
|
4348
|
-
nodes = store.listGoalPlanNodes(goal.goalId);
|
|
4463
|
+
nodes = syncReadyFlags(store, goal, store.listGoalPlanNodes(goal.goalId), opts);
|
|
4349
4464
|
if (budgetExhausted(goal)) {
|
|
4350
4465
|
goal = store.updateGoalStatus(goal.goalId, "budgetLimited", { daemonLeaseId: opts.daemonLeaseId });
|
|
4351
4466
|
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence), "goal token budget exhausted", startedAt);
|
|
4352
4467
|
}
|
|
4353
4468
|
const readyKeys = readyNodeKeys({
|
|
4354
|
-
status: goal
|
|
4469
|
+
status: planStatusForGoal(goal),
|
|
4355
4470
|
nodes
|
|
4356
4471
|
});
|
|
4357
4472
|
if (readyKeys.length > 0) {
|
|
@@ -4450,7 +4565,9 @@ ${result.stderr}`);
|
|
|
4450
4565
|
}
|
|
4451
4566
|
continue;
|
|
4452
4567
|
}
|
|
4453
|
-
const
|
|
4568
|
+
const diagnostic = noReadyDiagnostic(goal, nodes);
|
|
4569
|
+
lastDiagnostic = diagnostic;
|
|
4570
|
+
const blocker = diagnostic.blocker;
|
|
4454
4571
|
if (blocker === lastBlocker)
|
|
4455
4572
|
repeatedBlockerCount += 1;
|
|
4456
4573
|
else {
|
|
@@ -4462,15 +4579,16 @@ ${result.stderr}`);
|
|
|
4462
4579
|
turn,
|
|
4463
4580
|
phase: "status",
|
|
4464
4581
|
status: repeatedBlockerCount >= 3 ? "blocked" : "active",
|
|
4465
|
-
evidence:
|
|
4582
|
+
evidence: diagnostic
|
|
4466
4583
|
}, { daemonLeaseId: opts.daemonLeaseId });
|
|
4467
4584
|
if (repeatedBlockerCount >= 3) {
|
|
4468
4585
|
goal = store.updateGoalStatus(goal.goalId, "blocked", { daemonLeaseId: opts.daemonLeaseId });
|
|
4469
|
-
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence, validation), blocker, startedAt);
|
|
4586
|
+
return resultFromGoal(goal, "failed", stdoutFor(goal, nodes, evidence, validation, diagnostic), blocker, startedAt);
|
|
4470
4587
|
}
|
|
4471
4588
|
}
|
|
4472
4589
|
goal = store.updateGoalStatus(goal.goalId, "usageLimited", { daemonLeaseId: opts.daemonLeaseId });
|
|
4473
|
-
|
|
4590
|
+
const exhaustedError = lastDiagnostic?.blocker ?? (lastBlocker ? `${lastBlocker}; max turns exhausted` : "goal max turns exhausted");
|
|
4591
|
+
return resultFromGoal(goal, "failed", stdoutFor(goal, store.listGoalPlanNodes(goal.goalId), evidence, validation, lastDiagnostic), exhaustedError, startedAt);
|
|
4474
4592
|
}
|
|
4475
4593
|
|
|
4476
4594
|
// src/lib/workflow-runner.ts
|
|
@@ -5493,14 +5611,15 @@ function enableStartup(result) {
|
|
|
5493
5611
|
// package.json
|
|
5494
5612
|
var package_default = {
|
|
5495
5613
|
name: "@hasna/loops",
|
|
5496
|
-
version: "0.3.
|
|
5614
|
+
version: "0.3.57",
|
|
5497
5615
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5498
5616
|
type: "module",
|
|
5499
5617
|
main: "dist/index.js",
|
|
5500
5618
|
types: "dist/index.d.ts",
|
|
5501
5619
|
bin: {
|
|
5502
5620
|
loops: "dist/cli/index.js",
|
|
5503
|
-
"loops-daemon": "dist/daemon/index.js"
|
|
5621
|
+
"loops-daemon": "dist/daemon/index.js",
|
|
5622
|
+
"loops-mcp": "dist/mcp/index.js"
|
|
5504
5623
|
},
|
|
5505
5624
|
exports: {
|
|
5506
5625
|
".": {
|
|
@@ -5511,6 +5630,10 @@ var package_default = {
|
|
|
5511
5630
|
types: "./dist/sdk/index.d.ts",
|
|
5512
5631
|
import: "./dist/sdk/index.js"
|
|
5513
5632
|
},
|
|
5633
|
+
"./mcp": {
|
|
5634
|
+
types: "./dist/mcp/index.d.ts",
|
|
5635
|
+
import: "./dist/mcp/index.js"
|
|
5636
|
+
},
|
|
5514
5637
|
"./storage": {
|
|
5515
5638
|
types: "./dist/lib/store.d.ts",
|
|
5516
5639
|
import: "./dist/lib/store.js"
|
|
@@ -5523,7 +5646,7 @@ var package_default = {
|
|
|
5523
5646
|
"LICENSE"
|
|
5524
5647
|
],
|
|
5525
5648
|
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",
|
|
5649
|
+
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
5650
|
"build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
|
|
5528
5651
|
typecheck: "tsc --noEmit",
|
|
5529
5652
|
test: "bun test",
|
|
@@ -5559,6 +5682,7 @@ var package_default = {
|
|
|
5559
5682
|
dependencies: {
|
|
5560
5683
|
"@hasna/events": "^0.1.9",
|
|
5561
5684
|
"@hasna/machines": "0.0.49",
|
|
5685
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
5562
5686
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
5563
5687
|
ai: "6.0.204",
|
|
5564
5688
|
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";
|