@ouro.bot/cli 0.1.0-alpha.427 → 0.1.0-alpha.429
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.json +18 -0
- package/dist/heart/daemon/cli-exec.js +15 -202
- package/dist/heart/daemon/connect-bay.js +428 -0
- package/dist/heart/habits/habit-migration.js +11 -3
- package/dist/heart/habits/habit-parser.js +1 -1
- package/dist/heart/habits/habit-runtime-state.js +100 -0
- package/dist/heart/habits/habit-scheduler.js +3 -2
- package/dist/heart/hatch/hatch-flow.js +0 -1
- package/dist/heart/outlook/readers/runtime-readers.js +9 -25
- package/dist/mind/prompt.js +3 -1
- package/dist/senses/inner-dialog-worker.js +6 -16
- package/dist/senses/inner-dialog.js +3 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.429",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Habit `lastRun` now lives in bundle runtime state under `state/habits/<habit>.json` instead of being rewritten into tracked `habits/*.md` files after every habit turn.",
|
|
8
|
+
"The habit worker, scheduler, inner-dialog habit turns, outlook habit readers, and `ouro habit list` now resolve `lastRun` through one shared helper, with legacy habit frontmatter kept as read-only fallback until runtime state exists.",
|
|
9
|
+
"New habit definitions created by hatch flow or `ouro habit create` stop emitting `lastRun` in tracked frontmatter, and task-system habit migration now preserves historical `lastRun`/`last_run` values by importing them into runtime state instead.",
|
|
10
|
+
"Habit runtime-state coverage now explicitly protects the defensive strip/fallback paths, and the agent-facing prompt text now tells the truth that runtime timestamps live in `state/habits/` so tracked habit files stay declarative."
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"version": "0.1.0-alpha.428",
|
|
15
|
+
"changes": [
|
|
16
|
+
"Root `ouro connect` now renders through a dedicated width-aware connect-bay renderer with a framed header, one recommended next move, and separate `Provider core`, `Portable`, and `This machine` sections instead of flattening everything inside one long menu string.",
|
|
17
|
+
"The connect bay keeps using the same live provider verification truth path as `ouro up` and `ouro auth verify`, while tightening lane-specific status and next-action mapping so locked vaults, failed live checks, and missing setup each point at the right repair move.",
|
|
18
|
+
"Added direct connect-bay renderer coverage for wide, narrow, wrapping, lane-summary, and non-TTY branches, plus a nerves event so the new renderer stays under the same file-completeness and audit rules as the rest of the runtime.",
|
|
19
|
+
"Auth, cross-machine, and testing docs now describe the root connect surface as the framed responsive bay it really is, and `@ouro.bot/cli` plus the `ouro.bot` wrapper are version-synced for the connect-bay polish release."
|
|
20
|
+
]
|
|
21
|
+
},
|
|
4
22
|
{
|
|
5
23
|
"version": "0.1.0-alpha.427",
|
|
6
24
|
"changes": [
|
|
@@ -93,6 +93,7 @@ const stale_bundle_prune_1 = require("./stale-bundle-prune");
|
|
|
93
93
|
const up_progress_1 = require("./up-progress");
|
|
94
94
|
const provider_ping_1 = require("../provider-ping");
|
|
95
95
|
const agent_discovery_1 = require("./agent-discovery");
|
|
96
|
+
const connect_bay_1 = require("./connect-bay");
|
|
96
97
|
// ── ensureDaemonRunning ──
|
|
97
98
|
const DEFAULT_DAEMON_STARTUP_TIMEOUT_MS = 10_000;
|
|
98
99
|
const DEFAULT_DAEMON_STARTUP_POLL_INTERVAL_MS = 500;
|
|
@@ -1626,184 +1627,9 @@ function enableAgentSense(agent, sense, deps) {
|
|
|
1626
1627
|
fs.writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
|
|
1627
1628
|
}
|
|
1628
1629
|
const CONNECT_MENU_PROMPT = "Choose [1-6] or type a name: ";
|
|
1629
|
-
const CONNECT_TITLE_WIDTH = 34;
|
|
1630
|
-
const CONNECT_STATUS_PRIORITY = {
|
|
1631
|
-
"needs attention": 0,
|
|
1632
|
-
locked: 1,
|
|
1633
|
-
"needs credentials": 2,
|
|
1634
|
-
"needs setup": 3,
|
|
1635
|
-
missing: 4,
|
|
1636
|
-
"not attached": 5,
|
|
1637
|
-
ready: 6,
|
|
1638
|
-
attached: 6,
|
|
1639
|
-
};
|
|
1640
|
-
const RESET = "\x1b[0m";
|
|
1641
|
-
const BOLD = "\x1b[1m";
|
|
1642
|
-
const DIM = "\x1b[2m";
|
|
1643
|
-
const TEAL = "\x1b[38;2;78;201;176m";
|
|
1644
|
-
const GREEN = "\x1b[38;2;46;204;64m";
|
|
1645
|
-
const YELLOW = "\x1b[38;2;230;190;50m";
|
|
1646
|
-
/* v8 ignore start -- cosmetic ANSI wrappers @preserve */
|
|
1647
|
-
function connectBold(text) { return `${BOLD}${text}${RESET}`; }
|
|
1648
|
-
function connectDim(text) { return `${DIM}${text}${RESET}`; }
|
|
1649
|
-
function connectTeal(text) { return `${TEAL}${text}${RESET}`; }
|
|
1650
|
-
function connectGreen(text) { return `${GREEN}${text}${RESET}`; }
|
|
1651
|
-
function connectYellow(text) { return `${YELLOW}${text}${RESET}`; }
|
|
1652
|
-
/* v8 ignore stop */
|
|
1653
1630
|
function connectMenuIsTTY(deps) {
|
|
1654
1631
|
return deps.isTTY ?? process.stdout.isTTY === true;
|
|
1655
1632
|
}
|
|
1656
|
-
function connectStatusText(status, isTTY) {
|
|
1657
|
-
if (!isTTY)
|
|
1658
|
-
return status;
|
|
1659
|
-
if (status === "ready" || status === "attached")
|
|
1660
|
-
return connectGreen(status);
|
|
1661
|
-
if (status === "not attached")
|
|
1662
|
-
return connectDim(status);
|
|
1663
|
-
return connectYellow(status);
|
|
1664
|
-
}
|
|
1665
|
-
function connectSectionHeader(label, isTTY) {
|
|
1666
|
-
const rule = "─".repeat(Math.max(6, CONNECT_TITLE_WIDTH - label.length));
|
|
1667
|
-
if (!isTTY)
|
|
1668
|
-
return `${label}\n${rule}`;
|
|
1669
|
-
return ` ${connectTeal("──")} ${connectBold(label)} ${connectTeal(rule)}`;
|
|
1670
|
-
}
|
|
1671
|
-
function summarizeProviderLane(agent, lane, providerHealth) {
|
|
1672
|
-
if (lane.status === "unconfigured") {
|
|
1673
|
-
return {
|
|
1674
|
-
lane: lane.lane,
|
|
1675
|
-
status: "needs setup",
|
|
1676
|
-
title: "choose provider and model",
|
|
1677
|
-
detail: "needs setup",
|
|
1678
|
-
action: lane.repairCommand,
|
|
1679
|
-
};
|
|
1680
|
-
}
|
|
1681
|
-
const fallbackAction = lane.credential.repairCommand ?? providerHealth?.fix;
|
|
1682
|
-
if (lane.credential.status === "missing") {
|
|
1683
|
-
return {
|
|
1684
|
-
lane: lane.lane,
|
|
1685
|
-
status: "needs credentials",
|
|
1686
|
-
title: `${lane.provider} / ${lane.model}`,
|
|
1687
|
-
detail: "credentials missing",
|
|
1688
|
-
action: fallbackAction,
|
|
1689
|
-
};
|
|
1690
|
-
}
|
|
1691
|
-
if (lane.credential.status === "invalid-pool") {
|
|
1692
|
-
return {
|
|
1693
|
-
lane: lane.lane,
|
|
1694
|
-
status: "needs attention",
|
|
1695
|
-
title: `${lane.provider} / ${lane.model}`,
|
|
1696
|
-
detail: "vault unavailable",
|
|
1697
|
-
action: fallbackAction,
|
|
1698
|
-
};
|
|
1699
|
-
}
|
|
1700
|
-
if (lane.readiness.status === "failed") {
|
|
1701
|
-
return {
|
|
1702
|
-
lane: lane.lane,
|
|
1703
|
-
status: "needs attention",
|
|
1704
|
-
title: `${lane.provider} / ${lane.model}`,
|
|
1705
|
-
detail: `failed live check: ${lane.readiness.error ?? "unknown error"}`,
|
|
1706
|
-
action: providerHealth?.fix ?? `ouro auth --agent ${agent} --provider ${lane.provider}`,
|
|
1707
|
-
};
|
|
1708
|
-
}
|
|
1709
|
-
if (lane.readiness.status === "stale") {
|
|
1710
|
-
return {
|
|
1711
|
-
lane: lane.lane,
|
|
1712
|
-
status: "needs attention",
|
|
1713
|
-
title: `${lane.provider} / ${lane.model}`,
|
|
1714
|
-
detail: ["live check is stale", lane.readiness.reason].filter(Boolean).join(": "),
|
|
1715
|
-
action: providerHealth?.fix,
|
|
1716
|
-
};
|
|
1717
|
-
}
|
|
1718
|
-
if (lane.readiness.status === "ready") {
|
|
1719
|
-
return {
|
|
1720
|
-
lane: lane.lane,
|
|
1721
|
-
status: "ready",
|
|
1722
|
-
title: `${lane.provider} / ${lane.model}`,
|
|
1723
|
-
detail: "ready",
|
|
1724
|
-
};
|
|
1725
|
-
}
|
|
1726
|
-
return {
|
|
1727
|
-
lane: lane.lane,
|
|
1728
|
-
status: "needs attention",
|
|
1729
|
-
title: `${lane.provider} / ${lane.model}`,
|
|
1730
|
-
detail: "live check did not complete yet",
|
|
1731
|
-
action: providerHealth?.fix,
|
|
1732
|
-
};
|
|
1733
|
-
}
|
|
1734
|
-
function summarizeProvidersForConnect(agent, visibility, providerHealth) {
|
|
1735
|
-
const laneSummaries = visibility.lanes.map((lane) => summarizeProviderLane(agent, lane, providerHealth));
|
|
1736
|
-
const worstLaneStatus = laneSummaries.reduce((worst, lane) => CONNECT_STATUS_PRIORITY[lane.status] < CONNECT_STATUS_PRIORITY[worst] ? lane.status : worst, "ready");
|
|
1737
|
-
const providerHealthStatus = !providerHealth || providerHealth.ok
|
|
1738
|
-
? undefined
|
|
1739
|
-
: (() => {
|
|
1740
|
-
const error = String(providerHealth.error).toLowerCase();
|
|
1741
|
-
const fix = String(providerHealth.fix).toLowerCase();
|
|
1742
|
-
if (error.includes("failed live check"))
|
|
1743
|
-
return "needs attention";
|
|
1744
|
-
if (error.includes("has no credentials"))
|
|
1745
|
-
return "needs credentials";
|
|
1746
|
-
if (error.includes("missing") && error.includes("provider"))
|
|
1747
|
-
return "needs setup";
|
|
1748
|
-
if (error.includes("vault is locked") || error.includes("vault locked"))
|
|
1749
|
-
return "locked";
|
|
1750
|
-
if (fix.includes("ouro auth"))
|
|
1751
|
-
return "needs credentials";
|
|
1752
|
-
if (fix.includes("ouro use"))
|
|
1753
|
-
return "needs setup";
|
|
1754
|
-
if (fix.includes("vault unlock"))
|
|
1755
|
-
return "locked";
|
|
1756
|
-
return "needs attention";
|
|
1757
|
-
})();
|
|
1758
|
-
const nextLane = laneSummaries.find((lane) => lane.status !== "ready");
|
|
1759
|
-
return {
|
|
1760
|
-
status: providerHealthStatus ?? worstLaneStatus,
|
|
1761
|
-
detailLines: laneSummaries.flatMap((lane) => [
|
|
1762
|
-
`${lane.lane.padEnd(8)} ${lane.title}`,
|
|
1763
|
-
` ${lane.detail}`,
|
|
1764
|
-
]),
|
|
1765
|
-
nextAction: nextLane?.action ?? providerHealth?.fix,
|
|
1766
|
-
nextNote: nextLane ? `${nextLane.lane} ${nextLane.detail}` : undefined,
|
|
1767
|
-
};
|
|
1768
|
-
}
|
|
1769
|
-
function connectEntryNeedsAttention(entry) {
|
|
1770
|
-
return entry.status !== "ready" && entry.status !== "attached";
|
|
1771
|
-
}
|
|
1772
|
-
function connectNextEntry(entries) {
|
|
1773
|
-
return entries.find((entry) => connectEntryNeedsAttention(entry));
|
|
1774
|
-
}
|
|
1775
|
-
function appendConnectOptionalLine(lines, prefix, value) {
|
|
1776
|
-
if (typeof value === "string" && value.length > 0) {
|
|
1777
|
-
lines.push(` ${prefix}${value}`);
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
function connectNextMoveLines(entries, isTTY) {
|
|
1781
|
-
const next = connectNextEntry(entries);
|
|
1782
|
-
if (!next) {
|
|
1783
|
-
return [
|
|
1784
|
-
connectSectionHeader("Next best move", isTTY),
|
|
1785
|
-
" Everything here is ready. Pick what you want to review or refresh.",
|
|
1786
|
-
];
|
|
1787
|
-
}
|
|
1788
|
-
const lines = [
|
|
1789
|
-
connectSectionHeader("Next best move", isTTY),
|
|
1790
|
-
` ${next.name} - ${next.status}`,
|
|
1791
|
-
];
|
|
1792
|
-
appendConnectOptionalLine(lines, "", next.nextNote);
|
|
1793
|
-
appendConnectOptionalLine(lines, "run: ", next.nextAction);
|
|
1794
|
-
return lines;
|
|
1795
|
-
}
|
|
1796
|
-
function renderConnectEntry(entry, isTTY) {
|
|
1797
|
-
const label = `${entry.option}. ${entry.name}`.padEnd(25);
|
|
1798
|
-
const lines = [` ${label} ${connectStatusText(entry.status, isTTY)}`];
|
|
1799
|
-
for (const detail of entry.detailLines ?? []) {
|
|
1800
|
-
lines.push(` ${detail}`);
|
|
1801
|
-
}
|
|
1802
|
-
if (entry.description) {
|
|
1803
|
-
lines.push(isTTY ? ` ${connectDim(entry.description)}` : ` ${entry.description}`);
|
|
1804
|
-
}
|
|
1805
|
-
return lines;
|
|
1806
|
-
}
|
|
1807
1633
|
function readConnectBaySenseFlags(agent, deps) {
|
|
1808
1634
|
const configPath = path.join(providerCliAgentRoot({ agent }, deps), "agent.json");
|
|
1809
1635
|
const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
@@ -1831,7 +1657,7 @@ async function buildConnectMenu(agent, deps, onProgress) {
|
|
|
1831
1657
|
agentRoot: providerCliAgentRoot({ agent }, deps),
|
|
1832
1658
|
homeDir: providerCliHomeDir(deps),
|
|
1833
1659
|
});
|
|
1834
|
-
const providerSummary = summarizeProvidersForConnect(agent, providerVisibility, providerHealth);
|
|
1660
|
+
const providerSummary = (0, connect_bay_1.summarizeProvidersForConnect)(agent, providerVisibility, providerHealth);
|
|
1835
1661
|
onProgress?.("loading portable settings");
|
|
1836
1662
|
const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agent, { preserveCachedOnFailure: true });
|
|
1837
1663
|
onProgress?.("loading this machine's settings");
|
|
@@ -1865,6 +1691,7 @@ async function buildConnectMenu(agent, deps, onProgress) {
|
|
|
1865
1691
|
section: "Provider core",
|
|
1866
1692
|
status: providerSummary.status,
|
|
1867
1693
|
detailLines: providerSummary.detailLines,
|
|
1694
|
+
laneSummaries: providerSummary.laneSummaries,
|
|
1868
1695
|
nextAction: providerSummary.nextAction,
|
|
1869
1696
|
nextNote: providerSummary.nextNote,
|
|
1870
1697
|
},
|
|
@@ -1874,7 +1701,7 @@ async function buildConnectMenu(agent, deps, onProgress) {
|
|
|
1874
1701
|
section: "Portable",
|
|
1875
1702
|
status: perplexityStatus,
|
|
1876
1703
|
description: "Web search via Perplexity.",
|
|
1877
|
-
nextAction: connectEntryNeedsAttention({
|
|
1704
|
+
nextAction: (0, connect_bay_1.connectEntryNeedsAttention)({
|
|
1878
1705
|
option: "2",
|
|
1879
1706
|
name: "Perplexity search",
|
|
1880
1707
|
section: "Portable",
|
|
@@ -1887,7 +1714,7 @@ async function buildConnectMenu(agent, deps, onProgress) {
|
|
|
1887
1714
|
section: "Portable",
|
|
1888
1715
|
status: embeddingsStatus,
|
|
1889
1716
|
description: "Memory retrieval and note search.",
|
|
1890
|
-
nextAction: connectEntryNeedsAttention({
|
|
1717
|
+
nextAction: (0, connect_bay_1.connectEntryNeedsAttention)({
|
|
1891
1718
|
option: "3",
|
|
1892
1719
|
name: "Memory embeddings",
|
|
1893
1720
|
section: "Portable",
|
|
@@ -1900,7 +1727,7 @@ async function buildConnectMenu(agent, deps, onProgress) {
|
|
|
1900
1727
|
section: "Portable",
|
|
1901
1728
|
status: teamsStatus,
|
|
1902
1729
|
description: "Microsoft Teams sense credentials.",
|
|
1903
|
-
nextAction: connectEntryNeedsAttention({
|
|
1730
|
+
nextAction: (0, connect_bay_1.connectEntryNeedsAttention)({
|
|
1904
1731
|
option: "4",
|
|
1905
1732
|
name: "Teams",
|
|
1906
1733
|
section: "Portable",
|
|
@@ -1913,7 +1740,7 @@ async function buildConnectMenu(agent, deps, onProgress) {
|
|
|
1913
1740
|
section: "This machine",
|
|
1914
1741
|
status: blueBubblesStatus,
|
|
1915
1742
|
description: "Local Mac Messages bridge.",
|
|
1916
|
-
nextAction: connectEntryNeedsAttention({
|
|
1743
|
+
nextAction: (0, connect_bay_1.connectEntryNeedsAttention)({
|
|
1917
1744
|
option: "5",
|
|
1918
1745
|
name: "BlueBubbles iMessage",
|
|
1919
1746
|
section: "This machine",
|
|
@@ -1922,26 +1749,12 @@ async function buildConnectMenu(agent, deps, onProgress) {
|
|
|
1922
1749
|
},
|
|
1923
1750
|
];
|
|
1924
1751
|
const isTTY = connectMenuIsTTY(deps);
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
isTTY
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
...connectNextMoveLines(entries, isTTY),
|
|
1932
|
-
"",
|
|
1933
|
-
];
|
|
1934
|
-
for (const section of ["Provider core", "Portable", "This machine"]) {
|
|
1935
|
-
lines.push(connectSectionHeader(section, isTTY));
|
|
1936
|
-
for (const entry of entries.filter((candidate) => candidate.section === section)) {
|
|
1937
|
-
lines.push(...renderConnectEntry(entry, isTTY));
|
|
1938
|
-
lines.push("");
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
lines.push(isTTY ? ` ${connectDim("6. Not now")}` : " 6. Not now");
|
|
1942
|
-
lines.push("");
|
|
1943
|
-
lines.push(CONNECT_MENU_PROMPT);
|
|
1944
|
-
return lines.join("\n");
|
|
1752
|
+
return (0, connect_bay_1.renderConnectBay)(entries, {
|
|
1753
|
+
agent,
|
|
1754
|
+
isTTY,
|
|
1755
|
+
columns: deps.stdoutColumns ?? process.stdout.columns,
|
|
1756
|
+
prompt: CONNECT_MENU_PROMPT,
|
|
1757
|
+
});
|
|
1945
1758
|
}
|
|
1946
1759
|
async function executeConnectPerplexity(agent, deps) {
|
|
1947
1760
|
if (agent === "SerpentGuide") {
|
|
@@ -4194,6 +4007,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
4194
4007
|
// ── habit subcommands (local, no daemon socket needed) ──
|
|
4195
4008
|
if (command.kind === "habit.list" || command.kind === "habit.create") {
|
|
4196
4009
|
const { parseHabitFile, renderHabitFile } = await Promise.resolve().then(() => __importStar(require("../habits/habit-parser")));
|
|
4010
|
+
const { applyHabitRuntimeState } = await Promise.resolve().then(() => __importStar(require("../habits/habit-runtime-state")));
|
|
4197
4011
|
/* v8 ignore start -- production default: uses real bundle root @preserve */
|
|
4198
4012
|
const bundleRoot = deps.agentBundleRoot ?? path.join(deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`);
|
|
4199
4013
|
/* v8 ignore stop */
|
|
@@ -4216,7 +4030,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
4216
4030
|
const lines = [];
|
|
4217
4031
|
for (const file of files) {
|
|
4218
4032
|
const fileContent = fs.readFileSync(path.join(habitsDir, file), "utf-8");
|
|
4219
|
-
const habit = parseHabitFile(fileContent, path.join(habitsDir, file));
|
|
4033
|
+
const habit = applyHabitRuntimeState(bundleRoot, parseHabitFile(fileContent, path.join(habitsDir, file)));
|
|
4220
4034
|
const lastRunStr = habit.lastRun ?? "never";
|
|
4221
4035
|
lines.push(`${habit.name} cadence=${habit.cadence ?? "none"} status=${habit.status} lastRun=${lastRunStr}`);
|
|
4222
4036
|
}
|
|
@@ -4237,7 +4051,6 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
4237
4051
|
title: command.name,
|
|
4238
4052
|
cadence: command.cadence ?? "null",
|
|
4239
4053
|
status: "active",
|
|
4240
|
-
lastRun: now,
|
|
4241
4054
|
created: now,
|
|
4242
4055
|
}, `Habit: ${command.name}`);
|
|
4243
4056
|
fs.writeFileSync(filePath, habitContent, "utf-8");
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.summarizeProviderLane = summarizeProviderLane;
|
|
4
|
+
exports.summarizeProvidersForConnect = summarizeProvidersForConnect;
|
|
5
|
+
exports.connectEntryNeedsAttention = connectEntryNeedsAttention;
|
|
6
|
+
exports.renderConnectBay = renderConnectBay;
|
|
7
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
8
|
+
const CONNECT_STATUS_PRIORITY = {
|
|
9
|
+
"needs attention": 0,
|
|
10
|
+
locked: 1,
|
|
11
|
+
"needs credentials": 2,
|
|
12
|
+
"needs setup": 3,
|
|
13
|
+
missing: 4,
|
|
14
|
+
"not attached": 5,
|
|
15
|
+
ready: 6,
|
|
16
|
+
attached: 6,
|
|
17
|
+
};
|
|
18
|
+
const RESET = "\x1b[0m";
|
|
19
|
+
const BOLD = "\x1b[1m";
|
|
20
|
+
const TEAL = "\x1b[38;2;78;201;176m";
|
|
21
|
+
const GREEN = "\x1b[38;2;46;204;64m";
|
|
22
|
+
const GOLD = "\x1b[38;2;230;190;50m";
|
|
23
|
+
const BONE = "\x1b[38;2;238;242;234m";
|
|
24
|
+
const MIST = "\x1b[38;2;165;184;168m";
|
|
25
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
26
|
+
function stripAnsi(text) {
|
|
27
|
+
return text.replace(ANSI_RE, "");
|
|
28
|
+
}
|
|
29
|
+
function visibleLength(text) {
|
|
30
|
+
return stripAnsi(text).length;
|
|
31
|
+
}
|
|
32
|
+
function padAnsi(text, width) {
|
|
33
|
+
const missing = Math.max(0, width - visibleLength(text));
|
|
34
|
+
return `${text}${" ".repeat(missing)}`;
|
|
35
|
+
}
|
|
36
|
+
function wrapPlain(text, width) {
|
|
37
|
+
const normalized = text.trim();
|
|
38
|
+
if (!normalized)
|
|
39
|
+
return [""];
|
|
40
|
+
const words = normalized.split(/\s+/);
|
|
41
|
+
const lines = [];
|
|
42
|
+
let current = "";
|
|
43
|
+
for (const word of words) {
|
|
44
|
+
if (!current) {
|
|
45
|
+
current = word;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const candidate = `${current} ${word}`;
|
|
49
|
+
if (candidate.length <= width) {
|
|
50
|
+
current = candidate;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
lines.push(current);
|
|
54
|
+
current = word;
|
|
55
|
+
}
|
|
56
|
+
lines.push(current);
|
|
57
|
+
return lines;
|
|
58
|
+
}
|
|
59
|
+
function tty(text, color, bold = false) {
|
|
60
|
+
if (bold)
|
|
61
|
+
return `${color}${BOLD}${text}${RESET}`;
|
|
62
|
+
return `${color}${text}${RESET}`;
|
|
63
|
+
}
|
|
64
|
+
function escapeRegExp(value) {
|
|
65
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
66
|
+
}
|
|
67
|
+
function cleanExtractedCommand(command) {
|
|
68
|
+
const cleaned = command?.trim().replace(/[`'",;:.)]+$/g, "").trim();
|
|
69
|
+
return cleaned && cleaned.length > 0 ? cleaned : undefined;
|
|
70
|
+
}
|
|
71
|
+
function extractCommand(fixHint, commandPrefix) {
|
|
72
|
+
const escapedPrefix = escapeRegExp(commandPrefix);
|
|
73
|
+
const commandBody = `${escapedPrefix}(?=\\s|$)[^\`'"]*`;
|
|
74
|
+
const quoted = fixHint.match(new RegExp(`[\`'"](${commandBody})[\`'"]`, "i"))?.[1];
|
|
75
|
+
const unquoted = fixHint.match(new RegExp(`(${escapedPrefix}(?=\\s|$)[^\\n,;.]+)`, "i"))?.[1];
|
|
76
|
+
return cleanExtractedCommand(quoted) ?? cleanExtractedCommand(unquoted);
|
|
77
|
+
}
|
|
78
|
+
function resolveProviderHealthStatus(providerHealth) {
|
|
79
|
+
if (!providerHealth || providerHealth.ok)
|
|
80
|
+
return undefined;
|
|
81
|
+
const error = String(providerHealth.error).toLowerCase();
|
|
82
|
+
const fix = String(providerHealth.fix).toLowerCase();
|
|
83
|
+
if (error.includes("failed live check"))
|
|
84
|
+
return "needs attention";
|
|
85
|
+
if (error.includes("has no credentials"))
|
|
86
|
+
return "needs credentials";
|
|
87
|
+
if (error.includes("missing") && error.includes("provider"))
|
|
88
|
+
return "needs setup";
|
|
89
|
+
if (error.includes("vault is locked") || error.includes("vault locked"))
|
|
90
|
+
return "locked";
|
|
91
|
+
if (fix.includes("ouro auth verify"))
|
|
92
|
+
return "needs attention";
|
|
93
|
+
if (fix.includes("ouro auth"))
|
|
94
|
+
return "needs credentials";
|
|
95
|
+
if (fix.includes("ouro use"))
|
|
96
|
+
return "needs setup";
|
|
97
|
+
if (fix.includes("vault unlock"))
|
|
98
|
+
return "locked";
|
|
99
|
+
return "needs attention";
|
|
100
|
+
}
|
|
101
|
+
function resolveProviderHealthCommand(fixHint, status) {
|
|
102
|
+
if (!fixHint)
|
|
103
|
+
return undefined;
|
|
104
|
+
const prefixes = status === "locked"
|
|
105
|
+
? ["ouro vault unlock", "ouro vault replace", "ouro vault recover"]
|
|
106
|
+
: status === "needs credentials"
|
|
107
|
+
? ["ouro auth", "ouro connect", "ouro provider refresh", "ouro up"]
|
|
108
|
+
: status === "needs setup"
|
|
109
|
+
? ["ouro use", "ouro connect", "ouro auth", "ouro up"]
|
|
110
|
+
: ["ouro auth verify", "ouro auth", "ouro provider refresh", "ouro use", "ouro connect", "ouro vault unlock", "ouro up"];
|
|
111
|
+
for (const prefix of prefixes) {
|
|
112
|
+
const command = extractCommand(fixHint, prefix);
|
|
113
|
+
if (command)
|
|
114
|
+
return command;
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
function isProblemStatus(status) {
|
|
119
|
+
return status !== "ready" && status !== "attached";
|
|
120
|
+
}
|
|
121
|
+
function statusChip(status) {
|
|
122
|
+
const symbol = status === "ready" || status === "attached"
|
|
123
|
+
? "●"
|
|
124
|
+
: status === "not attached"
|
|
125
|
+
? "◌"
|
|
126
|
+
: "◆";
|
|
127
|
+
const label = `${symbol} ${status}`;
|
|
128
|
+
if (status === "ready" || status === "attached")
|
|
129
|
+
return tty(label, GREEN, true);
|
|
130
|
+
if (status === "not attached")
|
|
131
|
+
return tty(label, MIST);
|
|
132
|
+
return tty(label, GOLD, true);
|
|
133
|
+
}
|
|
134
|
+
function sectionTitle(title, width) {
|
|
135
|
+
const plain = `╭─ ${title} `;
|
|
136
|
+
const rule = "─".repeat(Math.max(0, width - plain.length - 1));
|
|
137
|
+
return `${tty("╭─ ", TEAL)}${tty(title, BONE, true)}${tty(` ${rule}╮`, TEAL)}`;
|
|
138
|
+
}
|
|
139
|
+
function bottomRule(width) {
|
|
140
|
+
const line = `╰${"─".repeat(Math.max(0, width - 2))}╯`;
|
|
141
|
+
return tty(line, TEAL);
|
|
142
|
+
}
|
|
143
|
+
function bodyLine(text, width) {
|
|
144
|
+
const padded = padAnsi(text, Math.max(0, width - 4));
|
|
145
|
+
return `${tty("│ ", TEAL)}${padded}${tty(" │", TEAL)}`;
|
|
146
|
+
}
|
|
147
|
+
function panel(title, body, width) {
|
|
148
|
+
const lines = [sectionTitle(title, width)];
|
|
149
|
+
for (const line of body) {
|
|
150
|
+
lines.push(bodyLine(line, width));
|
|
151
|
+
}
|
|
152
|
+
lines.push(bottomRule(width));
|
|
153
|
+
return lines;
|
|
154
|
+
}
|
|
155
|
+
function renderHeader(agent, width) {
|
|
156
|
+
return panel(`${agent} connect bay`, [
|
|
157
|
+
tty("Bring one capability online.", BONE, true),
|
|
158
|
+
tty("Everything on this screen was checked live just now.", MIST),
|
|
159
|
+
], width);
|
|
160
|
+
}
|
|
161
|
+
function nextMoveBody(entry) {
|
|
162
|
+
if (!entry) {
|
|
163
|
+
return [
|
|
164
|
+
tty("Everything here is ready.", BONE, true),
|
|
165
|
+
tty("Pick what you want to review or refresh.", MIST),
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
const lines = [
|
|
169
|
+
`${entry.name} ${statusChip(entry.status)}`,
|
|
170
|
+
];
|
|
171
|
+
if (entry.nextNote)
|
|
172
|
+
lines.push(entry.nextNote);
|
|
173
|
+
if (entry.nextAction)
|
|
174
|
+
lines.push(tty(entry.nextAction, MIST));
|
|
175
|
+
return lines;
|
|
176
|
+
}
|
|
177
|
+
function renderProviderBody(entry, width) {
|
|
178
|
+
const lines = [
|
|
179
|
+
`${entry.option} ${entry.name} ${statusChip(entry.status)}`,
|
|
180
|
+
];
|
|
181
|
+
const lanes = entry.laneSummaries ?? [];
|
|
182
|
+
for (const [index, lane] of lanes.entries()) {
|
|
183
|
+
if (index > 0)
|
|
184
|
+
lines.push("");
|
|
185
|
+
const laneLabel = lane.lane === "outward" ? "Outward lane" : "Inner lane";
|
|
186
|
+
lines.push(tty(laneLabel, BONE, true));
|
|
187
|
+
lines.push(lane.title);
|
|
188
|
+
lines.push(isProblemStatus(lane.status) ? lane.detail : tty(lane.detail, MIST));
|
|
189
|
+
}
|
|
190
|
+
if (lanes.length === 0) {
|
|
191
|
+
for (const detail of entry.detailLines ?? [])
|
|
192
|
+
lines.push(detail);
|
|
193
|
+
}
|
|
194
|
+
return normalizeWrappedBody(lines, width);
|
|
195
|
+
}
|
|
196
|
+
function renderCapabilityBody(entries, width) {
|
|
197
|
+
const lines = [];
|
|
198
|
+
for (const [index, entry] of entries.entries()) {
|
|
199
|
+
if (index > 0)
|
|
200
|
+
lines.push("");
|
|
201
|
+
lines.push(`${entry.option} ${entry.name} ${statusChip(entry.status)}`);
|
|
202
|
+
if (entry.description) {
|
|
203
|
+
lines.push(isProblemStatus(entry.status) ? entry.description : tty(entry.description, MIST));
|
|
204
|
+
}
|
|
205
|
+
for (const detail of entry.detailLines ?? []) {
|
|
206
|
+
lines.push(detail);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return normalizeWrappedBody(lines, width);
|
|
210
|
+
}
|
|
211
|
+
function normalizeWrappedBody(lines, width) {
|
|
212
|
+
const wrapped = [];
|
|
213
|
+
for (const line of lines) {
|
|
214
|
+
if (!line) {
|
|
215
|
+
wrapped.push("");
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const plain = stripAnsi(line);
|
|
219
|
+
if (plain.length <= width - 4) {
|
|
220
|
+
wrapped.push(line);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const segments = wrapPlain(plain, width - 4);
|
|
224
|
+
wrapped.push(...segments);
|
|
225
|
+
}
|
|
226
|
+
return wrapped;
|
|
227
|
+
}
|
|
228
|
+
function stackPanels(panels) {
|
|
229
|
+
const lines = [];
|
|
230
|
+
for (const [index, panelLines] of panels.entries()) {
|
|
231
|
+
if (index > 0)
|
|
232
|
+
lines.push("");
|
|
233
|
+
lines.push(...panelLines);
|
|
234
|
+
}
|
|
235
|
+
return lines;
|
|
236
|
+
}
|
|
237
|
+
function combineColumns(left, right, leftWidth, rightWidth, gap = 2) {
|
|
238
|
+
const total = Math.max(left.length, right.length);
|
|
239
|
+
const lines = [];
|
|
240
|
+
for (let index = 0; index < total; index += 1) {
|
|
241
|
+
const leftLine = left[index] ?? " ".repeat(leftWidth);
|
|
242
|
+
const rightLine = right[index] ?? " ".repeat(rightWidth);
|
|
243
|
+
lines.push(`${padAnsi(leftLine, leftWidth)}${" ".repeat(gap)}${padAnsi(rightLine, rightWidth)}`);
|
|
244
|
+
}
|
|
245
|
+
return lines;
|
|
246
|
+
}
|
|
247
|
+
function renderTtyBay(entries, options) {
|
|
248
|
+
const columns = Math.max(options.columns ?? 108, 72);
|
|
249
|
+
const fullWidth = Math.max(56, columns - 2);
|
|
250
|
+
const header = renderHeader(options.agent, fullWidth);
|
|
251
|
+
const nextEntry = entries.find((entry) => isProblemStatus(entry.status));
|
|
252
|
+
const providerEntry = entries.find((entry) => entry.section === "Provider core");
|
|
253
|
+
const portableEntries = entries.filter((entry) => entry.section === "Portable");
|
|
254
|
+
const machineEntries = entries.filter((entry) => entry.section === "This machine");
|
|
255
|
+
const wide = columns >= 118;
|
|
256
|
+
const footer = [
|
|
257
|
+
tty("Pick a path. Type the number or the name.", MIST),
|
|
258
|
+
options.prompt,
|
|
259
|
+
];
|
|
260
|
+
if (!wide) {
|
|
261
|
+
const panels = [
|
|
262
|
+
header,
|
|
263
|
+
panel("Next best move", nextMoveBody(nextEntry), fullWidth),
|
|
264
|
+
panel("Provider core", renderProviderBody(providerEntry, fullWidth), fullWidth),
|
|
265
|
+
panel("Portable", renderCapabilityBody(portableEntries, fullWidth), fullWidth),
|
|
266
|
+
panel("This machine", renderCapabilityBody(machineEntries, fullWidth), fullWidth),
|
|
267
|
+
];
|
|
268
|
+
return [...stackPanels(panels), "", ...footer].join("\n");
|
|
269
|
+
}
|
|
270
|
+
const gap = 2;
|
|
271
|
+
const leftWidth = Math.max(52, Math.floor((fullWidth - gap) / 2));
|
|
272
|
+
const rightWidth = Math.max(40, fullWidth - gap - leftWidth);
|
|
273
|
+
const topRow = combineColumns(panel("Next best move", nextMoveBody(nextEntry), leftWidth), panel("This machine", renderCapabilityBody(machineEntries, rightWidth), rightWidth), leftWidth, rightWidth, gap);
|
|
274
|
+
const bottomRow = combineColumns(panel("Provider core", renderProviderBody(providerEntry, leftWidth), leftWidth), panel("Portable", renderCapabilityBody(portableEntries, rightWidth), rightWidth), leftWidth, rightWidth, gap);
|
|
275
|
+
return [...header, "", ...topRow, "", ...bottomRow, "", ...footer].join("\n");
|
|
276
|
+
}
|
|
277
|
+
function renderNonTtyBay(entries, options) {
|
|
278
|
+
const nextEntry = entries.find((entry) => isProblemStatus(entry.status));
|
|
279
|
+
const lines = [
|
|
280
|
+
`${options.agent} connect bay`,
|
|
281
|
+
"Bring one capability online. Provider status was checked live just now.",
|
|
282
|
+
"",
|
|
283
|
+
"Next best move",
|
|
284
|
+
"--------------",
|
|
285
|
+
];
|
|
286
|
+
if (!nextEntry) {
|
|
287
|
+
lines.push("Everything here is ready. Pick what you want to review or refresh.");
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
lines.push(`${nextEntry.name} - ${nextEntry.status}`);
|
|
291
|
+
if (nextEntry.nextNote)
|
|
292
|
+
lines.push(nextEntry.nextNote);
|
|
293
|
+
if (nextEntry.nextAction)
|
|
294
|
+
lines.push(`run: ${nextEntry.nextAction}`);
|
|
295
|
+
}
|
|
296
|
+
lines.push("");
|
|
297
|
+
for (const section of ["Provider core", "Portable", "This machine"]) {
|
|
298
|
+
lines.push(section);
|
|
299
|
+
lines.push("-".repeat(Math.max(6, section.length + 4)));
|
|
300
|
+
for (const entry of entries.filter((candidate) => candidate.section === section)) {
|
|
301
|
+
lines.push(`${entry.option}. ${entry.name} [${entry.status}]`);
|
|
302
|
+
if (entry.laneSummaries && entry.laneSummaries.length > 0) {
|
|
303
|
+
for (const lane of entry.laneSummaries) {
|
|
304
|
+
const laneLabel = lane.lane === "outward" ? "Outward lane" : "Inner lane";
|
|
305
|
+
lines.push(` ${laneLabel}: ${lane.title}`);
|
|
306
|
+
lines.push(` ${lane.detail}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
for (const detail of entry.detailLines ?? [])
|
|
311
|
+
lines.push(` ${detail}`);
|
|
312
|
+
}
|
|
313
|
+
if (entry.description)
|
|
314
|
+
lines.push(` ${entry.description}`);
|
|
315
|
+
lines.push("");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
lines.push("6. Not now");
|
|
319
|
+
lines.push("");
|
|
320
|
+
lines.push("Pick a path. Type the number or the name.");
|
|
321
|
+
lines.push(options.prompt);
|
|
322
|
+
return lines.join("\n");
|
|
323
|
+
}
|
|
324
|
+
function summarizeProviderLane(agent, lane, providerHealth) {
|
|
325
|
+
const providerHealthStatus = resolveProviderHealthStatus(providerHealth);
|
|
326
|
+
const providerHealthCommand = resolveProviderHealthCommand(providerHealth?.fix, providerHealthStatus);
|
|
327
|
+
if (lane.status === "unconfigured") {
|
|
328
|
+
return {
|
|
329
|
+
lane: lane.lane,
|
|
330
|
+
status: "needs setup",
|
|
331
|
+
title: "choose provider and model",
|
|
332
|
+
detail: "needs setup",
|
|
333
|
+
action: lane.repairCommand,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const fallbackAction = providerHealthCommand ?? lane.credential.repairCommand;
|
|
337
|
+
if (lane.credential.status === "missing") {
|
|
338
|
+
return {
|
|
339
|
+
lane: lane.lane,
|
|
340
|
+
status: "needs credentials",
|
|
341
|
+
title: `${lane.provider} / ${lane.model}`,
|
|
342
|
+
detail: "credentials missing",
|
|
343
|
+
action: fallbackAction,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (lane.credential.status === "invalid-pool") {
|
|
347
|
+
return {
|
|
348
|
+
lane: lane.lane,
|
|
349
|
+
status: providerHealthStatus === "locked" ? "locked" : "needs attention",
|
|
350
|
+
title: `${lane.provider} / ${lane.model}`,
|
|
351
|
+
detail: providerHealthStatus === "locked" ? "vault locked on this machine" : "vault unavailable",
|
|
352
|
+
action: fallbackAction,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (lane.readiness.status === "failed") {
|
|
356
|
+
return {
|
|
357
|
+
lane: lane.lane,
|
|
358
|
+
status: "needs attention",
|
|
359
|
+
title: `${lane.provider} / ${lane.model}`,
|
|
360
|
+
detail: `failed live check: ${lane.readiness.error ?? "unknown error"}`,
|
|
361
|
+
action: providerHealth?.fix ?? `ouro auth --agent ${agent} --provider ${lane.provider}`,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
if (lane.readiness.status === "stale") {
|
|
365
|
+
return {
|
|
366
|
+
lane: lane.lane,
|
|
367
|
+
status: "needs attention",
|
|
368
|
+
title: `${lane.provider} / ${lane.model}`,
|
|
369
|
+
detail: ["live check is stale", lane.readiness.reason].filter(Boolean).join(": "),
|
|
370
|
+
action: providerHealth?.fix,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (lane.readiness.status === "ready") {
|
|
374
|
+
return {
|
|
375
|
+
lane: lane.lane,
|
|
376
|
+
status: "ready",
|
|
377
|
+
title: `${lane.provider} / ${lane.model}`,
|
|
378
|
+
detail: "ready",
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
lane: lane.lane,
|
|
383
|
+
status: "needs attention",
|
|
384
|
+
title: `${lane.provider} / ${lane.model}`,
|
|
385
|
+
detail: "live check did not complete yet",
|
|
386
|
+
action: providerHealth?.fix,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function summarizeProvidersForConnect(agent, visibility, providerHealth) {
|
|
390
|
+
const laneSummaries = visibility.lanes.map((lane) => summarizeProviderLane(agent, lane, providerHealth));
|
|
391
|
+
const worstLaneStatus = laneSummaries.reduce((worst, lane) => CONNECT_STATUS_PRIORITY[lane.status] < CONNECT_STATUS_PRIORITY[worst] ? lane.status : worst, "ready");
|
|
392
|
+
const providerHealthStatus = resolveProviderHealthStatus(providerHealth);
|
|
393
|
+
const providerHealthCommand = resolveProviderHealthCommand(providerHealth?.fix, providerHealthStatus);
|
|
394
|
+
const nextLane = laneSummaries.find((lane) => isProblemStatus(lane.status));
|
|
395
|
+
return {
|
|
396
|
+
status: providerHealthStatus ?? worstLaneStatus,
|
|
397
|
+
laneSummaries,
|
|
398
|
+
detailLines: laneSummaries.flatMap((lane) => [
|
|
399
|
+
`${lane.lane === "outward" ? "Outward lane" : "Inner lane"}: ${lane.title}`,
|
|
400
|
+
lane.detail,
|
|
401
|
+
]),
|
|
402
|
+
nextAction: providerHealthCommand ?? nextLane?.action,
|
|
403
|
+
nextNote: providerHealthStatus === "locked"
|
|
404
|
+
? "Unlock this agent's credential vault on this machine."
|
|
405
|
+
: nextLane
|
|
406
|
+
? `${nextLane.lane === "outward" ? "Outward lane" : "Inner lane"}: ${nextLane.detail}`
|
|
407
|
+
: undefined,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function connectEntryNeedsAttention(entry) {
|
|
411
|
+
return isProblemStatus(entry.status);
|
|
412
|
+
}
|
|
413
|
+
function renderConnectBay(entries, options) {
|
|
414
|
+
(0, runtime_1.emitNervesEvent)({
|
|
415
|
+
component: "daemon",
|
|
416
|
+
event: "daemon.connect_bay_rendered",
|
|
417
|
+
message: "rendered connect bay",
|
|
418
|
+
meta: {
|
|
419
|
+
agent: options.agent,
|
|
420
|
+
isTTY: options.isTTY,
|
|
421
|
+
entryCount: entries.length,
|
|
422
|
+
columns: options.columns ?? null,
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
if (!options.isTTY)
|
|
426
|
+
return renderNonTtyBay(entries, options);
|
|
427
|
+
return renderTtyBay(entries, options);
|
|
428
|
+
}
|
|
@@ -38,6 +38,7 @@ const fs = __importStar(require("fs"));
|
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const runtime_1 = require("../../nerves/runtime");
|
|
40
40
|
const habit_parser_1 = require("./habit-parser");
|
|
41
|
+
const habit_runtime_state_1 = require("./habit-runtime-state");
|
|
41
42
|
const parser_1 = require("../../repertoire/tasks/parser");
|
|
42
43
|
/** Fields that belong to the task system and should be stripped from migrated habits. */
|
|
43
44
|
const TASK_ONLY_FIELDS = new Set([
|
|
@@ -138,6 +139,11 @@ function migrateHabitsFromTaskSystem(bundleRoot) {
|
|
|
138
139
|
});
|
|
139
140
|
continue;
|
|
140
141
|
}
|
|
142
|
+
const legacyLastRun = typeof frontmatter.lastRun === "string" && frontmatter.lastRun !== "null"
|
|
143
|
+
? frontmatter.lastRun
|
|
144
|
+
: typeof frontmatter.last_run === "string" && frontmatter.last_run !== "null"
|
|
145
|
+
? frontmatter.last_run
|
|
146
|
+
: null;
|
|
141
147
|
// Build new frontmatter, stripping task-only fields
|
|
142
148
|
const newFrontmatter = {};
|
|
143
149
|
if (typeof frontmatter.title === "string")
|
|
@@ -145,14 +151,13 @@ function migrateHabitsFromTaskSystem(bundleRoot) {
|
|
|
145
151
|
if (typeof frontmatter.cadence === "string")
|
|
146
152
|
newFrontmatter.cadence = frontmatter.cadence;
|
|
147
153
|
newFrontmatter.status = habitStatus;
|
|
148
|
-
newFrontmatter.lastRun = typeof frontmatter.lastRun === "string" && frontmatter.lastRun !== "null"
|
|
149
|
-
? frontmatter.lastRun
|
|
150
|
-
: "null";
|
|
151
154
|
newFrontmatter.created = typeof frontmatter.created === "string" ? frontmatter.created : "null";
|
|
152
155
|
// Add any other non-task fields from original
|
|
153
156
|
for (const [key, value] of Object.entries(frontmatter)) {
|
|
154
157
|
if (TASK_ONLY_FIELDS.has(key))
|
|
155
158
|
continue;
|
|
159
|
+
if (key === "lastRun" || key === "last_run")
|
|
160
|
+
continue;
|
|
156
161
|
if (key in newFrontmatter)
|
|
157
162
|
continue;
|
|
158
163
|
/* v8 ignore next -- dead code: status is caught by `key in newFrontmatter` above since newFrontmatter.status is always set @preserve */
|
|
@@ -162,6 +167,9 @@ function migrateHabitsFromTaskSystem(bundleRoot) {
|
|
|
162
167
|
}
|
|
163
168
|
const rendered = (0, habit_parser_1.renderHabitFile)(newFrontmatter, body);
|
|
164
169
|
fs.writeFileSync(targetPath, rendered, "utf-8");
|
|
170
|
+
if (legacyLastRun) {
|
|
171
|
+
(0, habit_runtime_state_1.writeHabitLastRun)(bundleRoot, path.basename(slugName, ".md"), legacyLastRun);
|
|
172
|
+
}
|
|
165
173
|
migratedCount++;
|
|
166
174
|
(0, runtime_1.emitNervesEvent)({
|
|
167
175
|
component: "daemon",
|
|
@@ -98,7 +98,7 @@ function parseHabitFile(content, filePath) {
|
|
|
98
98
|
const cadence = typeof rawCadence === "string" && rawCadence.length > 0 ? rawCadence : null;
|
|
99
99
|
const rawStatus = frontmatter.status;
|
|
100
100
|
const status = typeof rawStatus === "string" && isHabitStatus(rawStatus) ? rawStatus : "active";
|
|
101
|
-
const rawLastRun = frontmatter.lastRun;
|
|
101
|
+
const rawLastRun = frontmatter.lastRun ?? frontmatter.last_run;
|
|
102
102
|
const lastRun = typeof rawLastRun === "string" && rawLastRun.length > 0 ? rawLastRun : null;
|
|
103
103
|
const rawCreated = frontmatter.created;
|
|
104
104
|
const created = typeof rawCreated === "string" && rawCreated.length > 0 ? rawCreated : null;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.readHabitLastRun = readHabitLastRun;
|
|
37
|
+
exports.applyHabitRuntimeState = applyHabitRuntimeState;
|
|
38
|
+
exports.writeHabitLastRun = writeHabitLastRun;
|
|
39
|
+
exports.recordHabitRun = recordHabitRun;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const json_store_1 = require("../../arc/json-store");
|
|
43
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
44
|
+
function habitRuntimeStateDir(agentRoot) {
|
|
45
|
+
return path.join(agentRoot, "state", "habits");
|
|
46
|
+
}
|
|
47
|
+
function isNonEmptyString(value) {
|
|
48
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
49
|
+
}
|
|
50
|
+
function stripLegacyLastRunFromDefinition(definitionPath) {
|
|
51
|
+
const content = fs.readFileSync(definitionPath, "utf-8");
|
|
52
|
+
const lines = content.split(/\r?\n/);
|
|
53
|
+
if (lines[0]?.trim() !== "---")
|
|
54
|
+
return;
|
|
55
|
+
const closing = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
56
|
+
if (closing === -1)
|
|
57
|
+
return;
|
|
58
|
+
const frontmatterLines = lines.slice(1, closing);
|
|
59
|
+
const filtered = frontmatterLines.filter((line) => !/^\s*lastRun\s*:/.test(line) && !/^\s*last_run\s*:/.test(line));
|
|
60
|
+
if (filtered.length === frontmatterLines.length)
|
|
61
|
+
return;
|
|
62
|
+
const nextContent = ["---", ...filtered, "---", ...lines.slice(closing + 1)].join("\n");
|
|
63
|
+
fs.writeFileSync(definitionPath, nextContent, "utf-8");
|
|
64
|
+
}
|
|
65
|
+
function readHabitLastRun(agentRoot, habitName) {
|
|
66
|
+
const record = (0, json_store_1.readJsonFile)(habitRuntimeStateDir(agentRoot), habitName);
|
|
67
|
+
return isNonEmptyString(record?.lastRun) ? record.lastRun : null;
|
|
68
|
+
}
|
|
69
|
+
function applyHabitRuntimeState(agentRoot, habit) {
|
|
70
|
+
const runtimeLastRun = readHabitLastRun(agentRoot, habit.name);
|
|
71
|
+
if (runtimeLastRun === null)
|
|
72
|
+
return habit;
|
|
73
|
+
return { ...habit, lastRun: runtimeLastRun };
|
|
74
|
+
}
|
|
75
|
+
function writeHabitLastRun(agentRoot, habitName, lastRun, updatedAt = lastRun) {
|
|
76
|
+
const record = {
|
|
77
|
+
schemaVersion: 1,
|
|
78
|
+
name: habitName,
|
|
79
|
+
lastRun,
|
|
80
|
+
updatedAt,
|
|
81
|
+
};
|
|
82
|
+
(0, json_store_1.writeJsonFile)(habitRuntimeStateDir(agentRoot), habitName, record);
|
|
83
|
+
(0, runtime_1.emitNervesEvent)({
|
|
84
|
+
component: "daemon",
|
|
85
|
+
event: "daemon.habit_runtime_state_write",
|
|
86
|
+
message: "wrote habit runtime state",
|
|
87
|
+
meta: { agentRoot, habitName, lastRun, updatedAt },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function recordHabitRun(agentRoot, habitName, lastRun, options = {}) {
|
|
91
|
+
writeHabitLastRun(agentRoot, habitName, lastRun);
|
|
92
|
+
if (!options.definitionPath)
|
|
93
|
+
return;
|
|
94
|
+
try {
|
|
95
|
+
stripLegacyLastRunFromDefinition(options.definitionPath);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Missing/deleted habit files should never block runtime-state recording.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -37,6 +37,7 @@ exports.HabitScheduler = void 0;
|
|
|
37
37
|
const path = __importStar(require("path"));
|
|
38
38
|
const runtime_1 = require("../../nerves/runtime");
|
|
39
39
|
const habit_parser_1 = require("./habit-parser");
|
|
40
|
+
const habit_runtime_state_1 = require("./habit-runtime-state");
|
|
40
41
|
const cadence_1 = require("../daemon/cadence");
|
|
41
42
|
const WATCH_DEBOUNCE_MS = 200;
|
|
42
43
|
class HabitScheduler {
|
|
@@ -167,7 +168,7 @@ class HabitScheduler {
|
|
|
167
168
|
const filePath = path.join(this.habitsDir, `${name}.md`);
|
|
168
169
|
try {
|
|
169
170
|
const content = this.deps.readFile(filePath, "utf-8");
|
|
170
|
-
return (0, habit_parser_1.parseHabitFile)(content, filePath);
|
|
171
|
+
return (0, habit_runtime_state_1.applyHabitRuntimeState)(path.dirname(this.habitsDir), (0, habit_parser_1.parseHabitFile)(content, filePath));
|
|
171
172
|
}
|
|
172
173
|
catch {
|
|
173
174
|
return null;
|
|
@@ -323,7 +324,7 @@ class HabitScheduler {
|
|
|
323
324
|
const filePath = path.join(this.habitsDir, file);
|
|
324
325
|
try {
|
|
325
326
|
const content = this.deps.readFile(filePath, "utf-8");
|
|
326
|
-
const habit = (0, habit_parser_1.parseHabitFile)(content, filePath);
|
|
327
|
+
const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(path.dirname(this.habitsDir), (0, habit_parser_1.parseHabitFile)(content, filePath));
|
|
327
328
|
habits.push(habit);
|
|
328
329
|
}
|
|
329
330
|
catch (error) {
|
|
@@ -85,7 +85,6 @@ function writeHeartbeatHabit(bundleRoot, now) {
|
|
|
85
85
|
title: "Heartbeat check-in",
|
|
86
86
|
cadence: "30m",
|
|
87
87
|
status: "active",
|
|
88
|
-
lastRun: "null",
|
|
89
88
|
created: now.toISOString(),
|
|
90
89
|
}, "Run a lightweight heartbeat cycle. Review task board and inbox.\nCheck on pending obligations. Journal anything important.");
|
|
91
90
|
fs.writeFileSync(filePath, content, "utf-8");
|
|
@@ -46,6 +46,8 @@ exports.readDeskPrefs = readDeskPrefs;
|
|
|
46
46
|
const fs = __importStar(require("fs"));
|
|
47
47
|
const path = __importStar(require("path"));
|
|
48
48
|
const runtime_1 = require("../../../nerves/runtime");
|
|
49
|
+
const habit_parser_1 = require("../../habits/habit-parser");
|
|
50
|
+
const habit_runtime_state_1 = require("../../habits/habit-runtime-state");
|
|
49
51
|
const identity_1 = require("../../identity");
|
|
50
52
|
const shared_1 = require("./shared");
|
|
51
53
|
const agent_machine_1 = require("./agent-machine");
|
|
@@ -467,10 +469,11 @@ function readHabitView(agentRoot, options = {}) {
|
|
|
467
469
|
if (!file.endsWith(".md"))
|
|
468
470
|
continue;
|
|
469
471
|
try {
|
|
470
|
-
const
|
|
471
|
-
const
|
|
472
|
-
if (
|
|
472
|
+
const filePath = path.join(habitsDir, file);
|
|
473
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
474
|
+
if (!/^---\r?\n[\s\S]*?\r?\n---/.test(raw))
|
|
473
475
|
continue;
|
|
476
|
+
const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(raw, filePath));
|
|
474
477
|
const cadenceMs = parseCadenceMs(habit.cadence);
|
|
475
478
|
let isOverdue = false;
|
|
476
479
|
let overdueMs = null;
|
|
@@ -482,10 +485,10 @@ function readHabitView(agentRoot, options = {}) {
|
|
|
482
485
|
}
|
|
483
486
|
}
|
|
484
487
|
items.push({
|
|
485
|
-
name: habit.name
|
|
486
|
-
title: habit.title
|
|
488
|
+
name: habit.name,
|
|
489
|
+
title: habit.title,
|
|
487
490
|
cadence: habit.cadence,
|
|
488
|
-
status: habit.status
|
|
491
|
+
status: habit.status,
|
|
489
492
|
lastRun: habit.lastRun,
|
|
490
493
|
bodyExcerpt: (0, shared_1.truncateExcerpt)(habit.body, 120),
|
|
491
494
|
isDegraded: false,
|
|
@@ -514,25 +517,6 @@ function readHabitView(agentRoot, options = {}) {
|
|
|
514
517
|
items,
|
|
515
518
|
};
|
|
516
519
|
}
|
|
517
|
-
function parseHabitFrontmatter(content) {
|
|
518
|
-
const fmMatch = /^---\n([\s\S]*?)\n---/.exec(content);
|
|
519
|
-
if (!fmMatch)
|
|
520
|
-
return null;
|
|
521
|
-
const fm = fmMatch[1];
|
|
522
|
-
const body = content.slice(fmMatch[0].length).trim() || null;
|
|
523
|
-
function extract(key) {
|
|
524
|
-
const match = new RegExp(`^${key}:\\s*(.+)$`, "m").exec(fm);
|
|
525
|
-
return match ? match[1].trim() : null;
|
|
526
|
-
}
|
|
527
|
-
return {
|
|
528
|
-
name: extract("name"),
|
|
529
|
-
title: extract("title"),
|
|
530
|
-
cadence: extract("cadence"),
|
|
531
|
-
status: extract("status"),
|
|
532
|
-
lastRun: extract("lastRun") ?? extract("last_run"),
|
|
533
|
-
body,
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
520
|
function readNeedsMeView(agentName, options = {}) {
|
|
537
521
|
const bundlesRoot = options.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
|
|
538
522
|
const now = options.now?.() ?? new Date();
|
package/dist/mind/prompt.js
CHANGED
|
@@ -1044,8 +1044,10 @@ when i'm done thinking and the attention queue is clear, i rest.
|
|
|
1044
1044
|
my habits live at habits/ — they're my autonomous rhythms. heartbeat
|
|
1045
1045
|
is my breathing, other habits are patterns i choose. i can read, create,
|
|
1046
1046
|
and modify them with read_file/write_file. the format is simple
|
|
1047
|
-
frontmatter (title, cadence, status,
|
|
1047
|
+
frontmatter (title, cadence, status, created) plus a body
|
|
1048
1048
|
that says what i do when the rhythm fires.
|
|
1049
|
+
runtime timestamps like lastRun live under state/habits/ so my tracked
|
|
1050
|
+
habit files stay declarative.
|
|
1049
1051
|
|
|
1050
1052
|
\`ouro habit list\` shows my current habits. \`ouro habit create\` makes
|
|
1051
1053
|
a new one. the cadence is personal — how often do i want each rhythm
|
|
@@ -35,13 +35,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.createInnerDialogWorker = createInnerDialogWorker;
|
|
37
37
|
exports.startInnerDialogWorker = startInnerDialogWorker;
|
|
38
|
-
const fs = __importStar(require("fs"));
|
|
39
38
|
const path = __importStar(require("path"));
|
|
40
39
|
const inner_dialog_1 = require("./inner-dialog");
|
|
41
40
|
const runtime_1 = require("../nerves/runtime");
|
|
42
41
|
const identity_1 = require("../heart/identity");
|
|
43
42
|
const pending_1 = require("../mind/pending");
|
|
44
|
-
const
|
|
43
|
+
const habit_runtime_state_1 = require("../heart/habits/habit-runtime-state");
|
|
45
44
|
function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runInnerDialogTurn)(options), hasPendingWork = () => (0, pending_1.hasPendingMessages)((0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)()))) {
|
|
46
45
|
let running = false;
|
|
47
46
|
const queue = [];
|
|
@@ -71,25 +70,16 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
|
|
|
71
70
|
},
|
|
72
71
|
});
|
|
73
72
|
}
|
|
74
|
-
//
|
|
73
|
+
// Record lastRun after a habit turn without dirtying the tracked habit file.
|
|
75
74
|
if (nextReason === "habit" && nextHabitName) {
|
|
76
75
|
try {
|
|
77
76
|
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const frontmatter = {
|
|
82
|
-
title: parsed.title,
|
|
83
|
-
cadence: parsed.cadence,
|
|
84
|
-
status: parsed.status,
|
|
85
|
-
lastRun: new Date().toISOString(),
|
|
86
|
-
created: parsed.created,
|
|
87
|
-
};
|
|
88
|
-
const rendered = (0, habit_parser_1.renderHabitFile)(frontmatter, parsed.body);
|
|
89
|
-
fs.writeFileSync(habitFilePath, rendered, "utf-8");
|
|
77
|
+
(0, habit_runtime_state_1.recordHabitRun)(agentRoot, nextHabitName, new Date().toISOString(), {
|
|
78
|
+
definitionPath: path.join(agentRoot, "habits", `${nextHabitName}.md`),
|
|
79
|
+
});
|
|
90
80
|
}
|
|
91
81
|
catch {
|
|
92
|
-
// Habit file may
|
|
82
|
+
// Habit file/state may be unavailable during the turn — skip gracefully
|
|
93
83
|
}
|
|
94
84
|
}
|
|
95
85
|
// Drain queue first
|
|
@@ -71,6 +71,7 @@ const bluebubbles_1 = require("./bluebubbles");
|
|
|
71
71
|
const habit_turn_message_1 = require("./habit-turn-message");
|
|
72
72
|
const journal_index_1 = require("../mind/journal-index");
|
|
73
73
|
const habit_parser_1 = require("../heart/habits/habit-parser");
|
|
74
|
+
const habit_runtime_state_1 = require("../heart/habits/habit-runtime-state");
|
|
74
75
|
const cadence_1 = require("../heart/daemon/cadence");
|
|
75
76
|
const daemon_health_1 = require("../heart/daemon/daemon-health");
|
|
76
77
|
const DEFAULT_INNER_DIALOG_INSTINCTS = [
|
|
@@ -493,7 +494,7 @@ function buildAlsoDueLine(agentRoot, currentHabitName, now) {
|
|
|
493
494
|
continue;
|
|
494
495
|
try {
|
|
495
496
|
const content = fs.readFileSync(path.join(habitsDir, file), "utf-8");
|
|
496
|
-
const habit = (0, habit_parser_1.parseHabitFile)(content, path.join(habitsDir, file));
|
|
497
|
+
const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(content, path.join(habitsDir, file)));
|
|
497
498
|
if (habit.status !== "active" || !habit.cadence)
|
|
498
499
|
continue;
|
|
499
500
|
const cadenceMs = (0, cadence_1.parseCadenceToMs)(habit.cadence);
|
|
@@ -568,7 +569,7 @@ async function runInnerDialogTurn(options) {
|
|
|
568
569
|
let habitLastRun = null;
|
|
569
570
|
try {
|
|
570
571
|
const habitContent = fs.readFileSync(habitFilePath, "utf-8");
|
|
571
|
-
const parsed = (0, habit_parser_1.parseHabitFile)(habitContent, habitFilePath);
|
|
572
|
+
const parsed = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(habitContent, habitFilePath));
|
|
572
573
|
habitBody = parsed.body || undefined;
|
|
573
574
|
habitTitle = parsed.title || habitName;
|
|
574
575
|
habitLastRun = parsed.lastRun;
|