@ouro.bot/cli 0.1.0-alpha.135 → 0.1.0-alpha.137

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 CHANGED
@@ -8,7 +8,8 @@ Ouroboros is a TypeScript harness for daemon-managed agents that live in externa
8
8
 
9
9
  - `npx ouro.bot` is the bootstrap path.
10
10
  - `ouro` is the installed day-to-day command.
11
- - `ouro up` starts or repairs the daemon, syncs the launcher, installs workflow helpers, and reconciles stale runtime state.
11
+ - `ouro up` starts the daemon from the installed production version, syncs the launcher, installs workflow helpers, and reconciles stale runtime state.
12
+ - `ouro dev` starts the daemon from a local repo build (skips update checker, writes plist to repo path).
12
13
  - Agent bundles live outside the repo at `~/AgentBundles/<agent>.ouro/`.
13
14
  - Secrets live outside the repo at `~/.agentsecrets/<agent>/secrets.json`.
14
15
  - Machine-scoped test and runtime spillover lives under `~/.agentstate/...`.
@@ -141,7 +142,10 @@ If you are changing runtime code, keep all three green.
141
142
  ## Common Commands
142
143
 
143
144
  ```bash
144
- ouro up
145
+ ouro up # start daemon from installed production version
146
+ ouro dev # start daemon from local repo build
147
+ ouro dev --repo-path /path # start from a specific repo checkout
148
+ ouro dev --clone # clone repo to ~/Projects/ouroboros, build, start
145
149
  ouro status
146
150
  ouro logs
147
151
  ouro stop
package/changelog.json CHANGED
@@ -1,6 +1,23 @@
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.137",
6
+ "changes": [
7
+ "ouro dev auto-discovers existing repo at ~/Projects/ouroboros or prompts for clone path.",
8
+ "ouro dev never clones without user consent — prompts in interactive mode, errors in non-interactive.",
9
+ "ouro dev --repo-path errors clearly when the specified path has no repo."
10
+ ]
11
+ },
12
+ {
13
+ "version": "0.1.0-alpha.136",
14
+ "changes": [
15
+ "Dev mode DX: ouro dev starts daemon from local repo build, skips update checker, prints dev mode indicator.",
16
+ "ouro up from dev context delegates to installed production binary or errors with install guidance.",
17
+ "Daemon accepts mode option; update checker suppressed in dev mode with daemon.update_checker_skip event.",
18
+ "Daemon entry emits daemon.dev_mode_indicator nerves event when running from a dev repo."
19
+ ]
20
+ },
4
21
  {
5
22
  "version": "0.1.0-alpha.135",
6
23
  "changes": [
@@ -272,6 +272,7 @@ function usage() {
272
272
  return [
273
273
  "Usage:",
274
274
  " ouro [up]",
275
+ " ouro dev [--repo-path <path>] [--clone [--clone-path <path>]]",
275
276
  " ouro stop|down|status|logs|hatch",
276
277
  " ouro -v|--version",
277
278
  " ouro config model --agent <name> <model-name>",
@@ -899,6 +900,27 @@ function parseOuroCommand(args) {
899
900
  }
900
901
  if (head === "up")
901
902
  return { kind: "daemon.up" };
903
+ if (head === "dev") {
904
+ const devArgs = args.slice(1);
905
+ let repoPath;
906
+ let clone = false;
907
+ let clonePath;
908
+ for (let i = 0; i < devArgs.length; i++) {
909
+ if (devArgs[i] === "--repo-path" && devArgs[i + 1]) {
910
+ repoPath = devArgs[++i];
911
+ continue;
912
+ }
913
+ if (devArgs[i] === "--clone") {
914
+ clone = true;
915
+ continue;
916
+ }
917
+ if (devArgs[i] === "--clone-path" && devArgs[i + 1]) {
918
+ clonePath = devArgs[++i];
919
+ continue;
920
+ }
921
+ }
922
+ return { kind: "daemon.dev", repoPath, clone, clonePath };
923
+ }
902
924
  if (head === "rollback")
903
925
  return { kind: "rollback", ...(second ? { version: second } : {}) };
904
926
  if (head === "versions")
@@ -1383,6 +1405,19 @@ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SO
1383
1405
  syncGlobalOuroBotWrapper: ouro_bot_global_installer_1.syncGlobalOuroBotWrapper,
1384
1406
  ensureSkillManagement: skill_management_installer_1.ensureSkillManagement,
1385
1407
  ensureDaemonBootPersistence: defaultEnsureDaemonBootPersistence,
1408
+ /* v8 ignore start -- dev-mode defaults: tests inject mocks for mode detection and binary resolution @preserve */
1409
+ detectMode: () => (0, runtime_mode_1.detectRuntimeMode)((0, identity_1.getRepoRoot)()),
1410
+ getInstalledBinaryPath: () => {
1411
+ const cliHome = (0, ouro_version_manager_1.getOuroCliHome)();
1412
+ const binaryPath = path.join(cliHome, "bin", "ouro");
1413
+ return fs.existsSync(binaryPath) ? binaryPath : null;
1414
+ },
1415
+ execInstalledBinary: (binaryPath, binArgs) => {
1416
+ const { execFileSync } = require("child_process");
1417
+ execFileSync(binaryPath, binArgs, { stdio: "inherit" });
1418
+ process.exit(0);
1419
+ },
1420
+ /* v8 ignore stop */
1386
1421
  /* v8 ignore next 3 -- integration: launches interactive CLI session @preserve */
1387
1422
  startChat: async (agentName) => {
1388
1423
  const { main } = await Promise.resolve().then(() => __importStar(require("../../senses/cli")));
@@ -1726,6 +1761,48 @@ function executeReminderCommand(command, taskMod) {
1726
1761
  return `error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
1727
1762
  }
1728
1763
  }
1764
+ /* v8 ignore start -- repo resolution for ouro dev: repoPath branch tested via daemon-cli-dev; clone requires real git/npm @preserve */
1765
+ function resolveDevRepoCwd(command, checkExists, deps) {
1766
+ if (command.repoPath)
1767
+ return path.resolve(command.repoPath);
1768
+ if (command.clone)
1769
+ return resolveClonePath(command, checkExists, deps);
1770
+ return deps.getRepoCwd ? deps.getRepoCwd() : (0, identity_1.getRepoRoot)();
1771
+ }
1772
+ /* v8 ignore stop */
1773
+ /* v8 ignore start -- clone/build: requires real git clone + npm install on disk @preserve */
1774
+ function resolveClonePath(command, checkExists, deps) {
1775
+ const cloneTarget = command.clonePath
1776
+ ? path.resolve(command.clonePath)
1777
+ : path.join(os.homedir(), "Projects", "ouroboros");
1778
+ if (!checkExists(path.join(cloneTarget, ".git"))) {
1779
+ deps.writeStdout(`cloning ouroboros to ${cloneTarget}...`);
1780
+ try {
1781
+ (0, child_process_1.execSync)(`git clone ${identity_1.HARNESS_CANONICAL_REPO_URL} "${cloneTarget}"`, { stdio: "inherit" });
1782
+ }
1783
+ catch {
1784
+ throw new Error(`clone failed. check your network and try again, or clone manually and use --repo-path.`);
1785
+ }
1786
+ }
1787
+ else {
1788
+ deps.writeStdout(`repo already exists at ${cloneTarget}, pulling latest...`);
1789
+ try {
1790
+ (0, child_process_1.execSync)("git pull --ff-only", { cwd: cloneTarget, stdio: "inherit" });
1791
+ }
1792
+ catch {
1793
+ deps.writeStdout("pull failed (may have local changes). continuing with existing code.");
1794
+ }
1795
+ }
1796
+ deps.writeStdout("building...");
1797
+ try {
1798
+ (0, child_process_1.execSync)("npm install && npm run build", { cwd: cloneTarget, stdio: "inherit" });
1799
+ }
1800
+ catch {
1801
+ throw new Error(`build failed in ${cloneTarget}. check the output above.`);
1802
+ }
1803
+ return cloneTarget;
1804
+ }
1805
+ /* v8 ignore stop */
1729
1806
  async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
1730
1807
  if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
1731
1808
  const text = usage();
@@ -1807,6 +1884,27 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
1807
1884
  meta: { kind: command.kind },
1808
1885
  });
1809
1886
  if (command.kind === "daemon.up") {
1887
+ // ── dev mode delegation: ouro up from a dev repo delegates to installed binary ──
1888
+ // Only runs when detectMode is explicitly injected (via createDefaultOuroCliDeps or tests)
1889
+ if (deps.detectMode) {
1890
+ const runtimeMode = deps.detectMode();
1891
+ if (runtimeMode === "dev") {
1892
+ /* v8 ignore next -- defensive: getInstalledBinaryPath always injected in tests @preserve */
1893
+ const installedBinary = deps.getInstalledBinaryPath ? deps.getInstalledBinaryPath() : null;
1894
+ if (installedBinary) {
1895
+ deps.writeStdout("delegating to installed ouro...");
1896
+ /* v8 ignore next 3 -- defensive: execInstalledBinary always injected; missing branch unreachable @preserve */
1897
+ if (deps.execInstalledBinary) {
1898
+ deps.execInstalledBinary(installedBinary, args);
1899
+ }
1900
+ /* v8 ignore next 2 -- unreachable after exec replaces process @preserve */
1901
+ return "";
1902
+ }
1903
+ const message = "no installed version found. run: npx @ouro.bot/cli@alpha";
1904
+ deps.writeStdout(message);
1905
+ return message;
1906
+ }
1907
+ }
1810
1908
  const linkedVersionBeforeUp = deps.getCurrentCliVersion?.() ?? null;
1811
1909
  // ── versioned CLI update check ──
1812
1910
  if (deps.checkForCliUpdate) {
@@ -1900,6 +1998,96 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
1900
1998
  deps.writeStdout(daemonResult.message);
1901
1999
  return daemonResult.message;
1902
2000
  }
2001
+ if (command.kind === "daemon.dev") {
2002
+ /* v8 ignore next -- defensive: existsSync always injected in tests @preserve */
2003
+ const checkExists = deps.existsSync ?? fs.existsSync;
2004
+ /* v8 ignore next -- repo resolution dispatched to v8-ignored helper @preserve */
2005
+ let repoCwd = resolveDevRepoCwd(command, checkExists, deps);
2006
+ let entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
2007
+ if (!checkExists(entryPath) || !checkExists(path.join(repoCwd, ".git"))) {
2008
+ if (command.repoPath) {
2009
+ // Explicit --repo-path didn't have a valid repo — error
2010
+ const message = `no harness repo found at ${repoCwd}. run npm run build first.`;
2011
+ deps.writeStdout(message);
2012
+ return message;
2013
+ }
2014
+ /* v8 ignore start -- auto-clone: interactive prompt + existing repo discovery + real git/npm @preserve */
2015
+ const defaultClonePath = path.join(os.homedir(), "Projects", "ouroboros");
2016
+ if (checkExists(path.join(defaultClonePath, ".git"))) {
2017
+ deps.writeStdout(`found existing repo at ${defaultClonePath}`);
2018
+ try {
2019
+ repoCwd = resolveClonePath({ clonePath: defaultClonePath }, checkExists, deps);
2020
+ entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
2021
+ }
2022
+ catch (err) {
2023
+ const message = err instanceof Error ? err.message : String(err);
2024
+ deps.writeStdout(message);
2025
+ return message;
2026
+ }
2027
+ }
2028
+ else if (deps.promptInput) {
2029
+ deps.writeStdout("no harness repo found.");
2030
+ const answer = await deps.promptInput(`already have a checkout? enter its path, or press enter to clone to ${defaultClonePath}: `);
2031
+ const cloneTarget = answer.trim() || defaultClonePath;
2032
+ try {
2033
+ repoCwd = resolveClonePath({ clonePath: cloneTarget }, checkExists, deps);
2034
+ entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
2035
+ }
2036
+ catch (err) {
2037
+ const message = err instanceof Error ? err.message : String(err);
2038
+ deps.writeStdout(message);
2039
+ return message;
2040
+ }
2041
+ }
2042
+ else {
2043
+ const message = `no harness repo found. run: ouro dev --repo-path /path/to/ouroboros`;
2044
+ deps.writeStdout(message);
2045
+ return message;
2046
+ }
2047
+ /* v8 ignore stop */
2048
+ }
2049
+ /* v8 ignore start -- defensive: ensureDaemonBootPersistence always injected in tests @preserve */
2050
+ if (deps.ensureDaemonBootPersistence) {
2051
+ try {
2052
+ await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
2053
+ /* v8 ignore next -- defensive: boot persistence error should not block dev mode @preserve */
2054
+ }
2055
+ catch (error) {
2056
+ (0, runtime_1.emitNervesEvent)({
2057
+ level: "warn",
2058
+ component: "daemon",
2059
+ event: "daemon.dev_boot_persistence_error",
2060
+ message: "failed to persist daemon boot startup in dev mode",
2061
+ meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
2062
+ });
2063
+ }
2064
+ /* v8 ignore stop */
2065
+ }
2066
+ // Always force-restart in dev mode — you rebuilt, you want this code running
2067
+ /* v8 ignore start -- dev force-restart: socket alive/stop/spawn tested via integration; tests inject mocks @preserve */
2068
+ const alive = await deps.checkSocketAlive(deps.socketPath);
2069
+ if (alive) {
2070
+ try {
2071
+ await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
2072
+ }
2073
+ catch { /* already stopping */ }
2074
+ }
2075
+ deps.cleanupStaleSocket(deps.socketPath);
2076
+ const devEntry = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
2077
+ const startDevDaemon = deps.startDaemonProcess === defaultStartDaemonProcess
2078
+ ? async (sp) => {
2079
+ const child = (0, child_process_1.spawn)("node", [devEntry, "--socket", sp], { detached: true, stdio: "ignore" });
2080
+ child.unref();
2081
+ return { pid: child.pid ?? null };
2082
+ }
2083
+ : deps.startDaemonProcess;
2084
+ /* v8 ignore stop */
2085
+ const started = await startDevDaemon(deps.socketPath);
2086
+ /* v8 ignore next -- defensive: pid is null only when spawn fails silently @preserve */
2087
+ const message = `daemon running in dev mode from ${repoCwd} (pid ${started.pid ?? "unknown"})`;
2088
+ deps.writeStdout(message);
2089
+ return message;
2090
+ }
1903
2091
  // ── rollback command (local, no daemon socket needed for symlinks) ──
1904
2092
  /* v8 ignore start -- rollback/versions: tested via daemon-cli-rollback/versions tests @preserve */
1905
2093
  if (command.kind === "rollback") {
@@ -66,6 +66,16 @@ const mode = (0, runtime_mode_1.detectRuntimeMode)((0, identity_1.getRepoRoot)()
66
66
  message: "starting daemon entrypoint",
67
67
  meta: { socketPath, entryPath, mode },
68
68
  });
69
+ /* v8 ignore next -- dev-mode indicator: false branch (production) tested in daemon-boot-updates.test.ts @preserve */
70
+ if (mode === "dev") {
71
+ const repoRoot = (0, identity_1.getRepoRoot)();
72
+ (0, runtime_1.emitNervesEvent)({
73
+ component: "daemon",
74
+ event: "daemon.dev_mode_indicator",
75
+ message: `[dev] running from ${repoRoot}`,
76
+ meta: { repoRoot },
77
+ });
78
+ }
69
79
  const managedAgents = (0, agent_discovery_1.listEnabledBundleAgents)();
70
80
  const processManager = new process_manager_1.DaemonProcessManager({
71
81
  agents: managedAgents.map((agent) => ({
@@ -103,6 +113,7 @@ const daemon = new daemon_1.OuroDaemon({
103
113
  scheduler,
104
114
  healthMonitor,
105
115
  router,
116
+ mode,
106
117
  });
107
118
  void daemon.start().catch(async () => {
108
119
  (0, runtime_1.emitNervesEvent)({
@@ -174,6 +174,7 @@ class OuroDaemon {
174
174
  router;
175
175
  senseManager;
176
176
  bundlesRoot;
177
+ mode;
177
178
  server = null;
178
179
  constructor(options) {
179
180
  this.socketPath = options.socketPath;
@@ -183,6 +184,7 @@ class OuroDaemon {
183
184
  this.router = options.router;
184
185
  this.senseManager = options.senseManager ?? null;
185
186
  this.bundlesRoot = options.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
187
+ this.mode = options.mode ?? "production";
186
188
  }
187
189
  async start() {
188
190
  if (this.server)
@@ -198,39 +200,50 @@ class OuroDaemon {
198
200
  const currentVersion = (0, bundle_manifest_1.getPackageVersion)();
199
201
  await (0, update_hooks_1.applyPendingUpdates)(this.bundlesRoot, currentVersion);
200
202
  // Start periodic update checker (polls npm registry every 30 minutes)
203
+ // Skip in dev mode — dev builds should not auto-update from npm
201
204
  const bundlesRoot = this.bundlesRoot;
202
- const daemon = this;
203
- (0, update_checker_1.startUpdateChecker)({
204
- currentVersion,
205
- deps: {
206
- distTag: "alpha",
207
- fetchRegistryJson: /* v8 ignore next -- integration: real HTTP fetch @preserve */ async () => {
208
- const res = await fetch("https://registry.npmjs.org/@ouro.bot/cli");
209
- return res.json();
210
- },
211
- },
212
- onUpdate: /* v8 ignore start -- integration: real npm install + process spawn @preserve */ async (result) => {
213
- if (!result.latestVersion)
214
- return;
215
- await (0, staged_restart_1.performStagedRestart)(result.latestVersion, {
216
- execSync: (cmd) => (0, child_process_1.execSync)(cmd, { stdio: "inherit" }),
217
- spawnSync: child_process_1.spawnSync,
218
- resolveNewCodePath: (_version) => {
219
- try {
220
- const resolved = (0, child_process_1.execSync)(`node -e "console.log(require.resolve('@ouro.bot/cli/package.json'))"`, { encoding: "utf-8" }).trim();
221
- return resolved ? path.dirname(resolved) : null;
222
- }
223
- catch {
224
- return null;
225
- }
205
+ if (this.mode === "dev") {
206
+ (0, runtime_1.emitNervesEvent)({
207
+ component: "daemon",
208
+ event: "daemon.update_checker_skip",
209
+ message: "skipping update checker in dev mode",
210
+ meta: { reason: "dev mode" },
211
+ });
212
+ }
213
+ else {
214
+ const daemon = this;
215
+ (0, update_checker_1.startUpdateChecker)({
216
+ currentVersion,
217
+ deps: {
218
+ distTag: "alpha",
219
+ fetchRegistryJson: /* v8 ignore next -- integration: real HTTP fetch @preserve */ async () => {
220
+ const res = await fetch("https://registry.npmjs.org/@ouro.bot/cli");
221
+ return res.json();
226
222
  },
227
- gracefulShutdown: () => daemon.stop(),
228
- nodePath: process.execPath,
229
- bundlesRoot,
230
- });
231
- },
232
- /* v8 ignore stop */
233
- });
223
+ },
224
+ onUpdate: /* v8 ignore start -- integration: real npm install + process spawn @preserve */ async (result) => {
225
+ if (!result.latestVersion)
226
+ return;
227
+ await (0, staged_restart_1.performStagedRestart)(result.latestVersion, {
228
+ execSync: (cmd) => (0, child_process_1.execSync)(cmd, { stdio: "inherit" }),
229
+ spawnSync: child_process_1.spawnSync,
230
+ resolveNewCodePath: (_version) => {
231
+ try {
232
+ const resolved = (0, child_process_1.execSync)(`node -e "console.log(require.resolve('@ouro.bot/cli/package.json'))"`, { encoding: "utf-8" }).trim();
233
+ return resolved ? path.dirname(resolved) : null;
234
+ }
235
+ catch {
236
+ return null;
237
+ }
238
+ },
239
+ gracefulShutdown: () => daemon.stop(),
240
+ nodePath: process.execPath,
241
+ bundlesRoot,
242
+ });
243
+ },
244
+ /* v8 ignore stop */
245
+ });
246
+ }
234
247
  // Pre-initialize MCP connections so they're ready for the first command (non-blocking)
235
248
  /* v8 ignore next -- catch callback: getSharedMcpManager logs errors internally @preserve */
236
249
  (0, mcp_manager_1.getSharedMcpManager)().catch(() => { });
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- // Thin entrypoint for `npm run dev` / `node dist/senses/cli-entry.js --agent <name>`.
2
+ // Thin entrypoint for `npm run cli` / `node dist/senses/cli-entry.js --agent <name>`.
3
3
  // Separated from cli.ts so the CLI adapter is pure library code with clean
4
4
  // 100% test coverage -- entrypoints can't be covered by vitest since
5
5
  // require.main !== module in the test runner.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.135",
3
+ "version": "0.1.0-alpha.137",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",
@@ -19,7 +19,8 @@
19
19
  "./runOuroCli": "./dist/heart/daemon/daemon-cli.js"
20
20
  },
21
21
  "scripts": {
22
- "dev": "tsc && node dist/senses/cli-entry.js --agent ouroboros",
22
+ "dev": "tsc && node dist/heart/daemon/ouro-bot-entry.js dev",
23
+ "cli": "tsc && node dist/senses/cli-entry.js",
23
24
  "daemon": "tsc && node dist/heart/daemon/daemon-entry.js",
24
25
  "ouro": "tsc && node dist/heart/daemon/ouro-entry.js",
25
26
  "teams": "tsc && node dist/senses/teams-entry.js --agent ouroboros",