@ouro.bot/cli 0.1.0-alpha.361 → 0.1.0-alpha.362

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 CHANGED
@@ -1,6 +1,16 @@
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.362",
6
+ "changes": [
7
+ "New `ouro clone <remote>` command for cross-machine agent setup: clones bundle from git remote, creates machine identity, enables sync, guides to auth flow. Infers agent name from URL.",
8
+ "WSL-aware `ouro setup --tool claude-code`: detects WSL2, resolves Windows-side home, calls claude.exe with wsl-prefixed MCP serve and hook commands, writes settings to Windows .claude/ directory.",
9
+ "Platform detection module (detectPlatform) returning macos/linux/wsl/windows-native with injectable deps.",
10
+ "First-run hatch-or-clone interactive choice when no bundles exist. Manual-clone detection during `ouro up` offers to enable sync on git-cloned bundles.",
11
+ "npx ouro.bot bootstrap now passes through to CLI on first install instead of stopping early."
12
+ ]
13
+ },
4
14
  {
5
15
  "version": "0.1.0-alpha.361",
6
16
  "changes": [
@@ -450,6 +450,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
450
450
  // Refresh system prompt at start of each turn when channel is provided.
451
451
  // If refresh fails, keep existing system prompt (or inject a minimal safe fallback)
452
452
  // so turn execution remains consistent and non-fatal.
453
+ let structuredSystemPrompt;
453
454
  if (channel) {
454
455
  try {
455
456
  const buildSystemOptions = {
@@ -458,7 +459,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
458
459
  supportedReasoningEfforts: providerRuntime.supportedReasoningEfforts,
459
460
  };
460
461
  const refreshed = await (0, prompt_1.buildSystem)(channel, buildSystemOptions, currentContext);
461
- upsertSystemPrompt(messages, refreshed);
462
+ structuredSystemPrompt = refreshed;
463
+ upsertSystemPrompt(messages, (0, prompt_1.flattenSystemPrompt)(refreshed));
462
464
  }
463
465
  catch (error) {
464
466
  const hadExistingSystemPrompt = messages[0]?.role === "system" && typeof messages[0].content === "string";
@@ -611,6 +613,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
611
613
  toolChoiceRequired,
612
614
  reasoningEffort: currentReasoningEffort,
613
615
  eagerSettleStreaming: true,
616
+ systemPrompt: structuredSystemPrompt,
614
617
  });
615
618
  }
616
619
  catch (error) {
@@ -42,6 +42,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.mergeStartupStability = mergeStartupStability;
43
43
  exports.ensureDaemonRunning = ensureDaemonRunning;
44
44
  exports.listGithubCopilotModels = listGithubCopilotModels;
45
+ exports.checkManualCloneBundles = checkManualCloneBundles;
45
46
  exports.runOuroCli = runOuroCli;
46
47
  const child_process_1 = require("child_process");
47
48
  const crypto_1 = require("crypto");
@@ -54,6 +55,7 @@ const runtime_1 = require("../../nerves/runtime");
54
55
  const store_file_1 = require("../../mind/friends/store-file");
55
56
  const runtime_metadata_1 = require("./runtime-metadata");
56
57
  const runtime_mode_1 = require("./runtime-mode");
58
+ const platform_1 = require("../platform");
57
59
  const daemon_runtime_sync_1 = require("./daemon-runtime-sync");
58
60
  const update_hooks_1 = require("../versioning/update-hooks");
59
61
  const bundle_meta_1 = require("./hooks/bundle-meta");
@@ -441,7 +443,78 @@ async function verifyProviderCredentials(provider, providers) {
441
443
  return `failed (${error instanceof Error ? error.message : String(error)})`;
442
444
  }
443
445
  }
444
- /* v8 ignore stop */
446
+ async function checkManualCloneBundles(deps) {
447
+ if (!deps.promptInput)
448
+ return;
449
+ let entries;
450
+ try {
451
+ entries = fs.readdirSync(deps.bundlesRoot).filter((e) => e.endsWith(".ouro"));
452
+ }
453
+ catch {
454
+ return;
455
+ }
456
+ for (const agentDir of entries) {
457
+ const bundlePath = path.join(deps.bundlesRoot, agentDir);
458
+ const gitDir = path.join(bundlePath, ".git");
459
+ if (!fs.existsSync(gitDir))
460
+ continue;
461
+ // Check for remotes
462
+ let remoteOutput;
463
+ try {
464
+ remoteOutput = (0, child_process_1.execFileSync)("git", ["remote", "-v"], { cwd: bundlePath, stdio: "pipe" }).toString().trim();
465
+ }
466
+ catch {
467
+ continue;
468
+ }
469
+ if (!remoteOutput)
470
+ continue;
471
+ // Check if sync is already enabled
472
+ const agentJsonPath = path.join(bundlePath, "agent.json");
473
+ if (fs.existsSync(agentJsonPath)) {
474
+ try {
475
+ const raw = fs.readFileSync(agentJsonPath, "utf-8");
476
+ const config = JSON.parse(raw);
477
+ if (config.sync?.enabled)
478
+ continue;
479
+ }
480
+ catch {
481
+ // Can't read agent.json — skip
482
+ continue;
483
+ }
484
+ }
485
+ // Parse first remote name
486
+ const firstLine = remoteOutput.split("\n")[0];
487
+ /* v8 ignore next -- defensive fallback: .trim() above strips leading tabs so empty-field path is unreachable @preserve */
488
+ const remoteName = firstLine.split("\t")[0] || "origin";
489
+ (0, runtime_1.emitNervesEvent)({
490
+ component: "daemon",
491
+ event: "daemon.manual_clone_detected",
492
+ message: "bundle appears to be a manually cloned git repo",
493
+ meta: { agent: agentDir, remote: remoteName },
494
+ });
495
+ const answer = await deps.promptInput(`Bundle ${agentDir} appears to be a git clone with a remote. Enable sync? (y/n): `);
496
+ if (answer.trim().toLowerCase() === "y") {
497
+ const raw = fs.readFileSync(agentJsonPath, "utf-8");
498
+ const config = JSON.parse(raw);
499
+ config.sync = { enabled: true, remote: remoteName };
500
+ fs.writeFileSync(agentJsonPath, JSON.stringify(config, null, 2) + "\n");
501
+ (0, runtime_1.emitNervesEvent)({
502
+ component: "daemon",
503
+ event: "daemon.manual_clone_sync_enabled",
504
+ message: "sync enabled for manually cloned bundle",
505
+ meta: { agent: agentDir, remote: remoteName },
506
+ });
507
+ }
508
+ else {
509
+ (0, runtime_1.emitNervesEvent)({
510
+ component: "daemon",
511
+ event: "daemon.manual_clone_sync_skipped",
512
+ message: "user declined sync for manually cloned bundle",
513
+ meta: { agent: agentDir },
514
+ });
515
+ }
516
+ }
517
+ }
445
518
  // ── toDaemonCommand ──
446
519
  function toDaemonCommand(command) {
447
520
  return command;
@@ -1270,6 +1343,28 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
1270
1343
  if (args.length === 0) {
1271
1344
  const discovered = await Promise.resolve(deps.listDiscoveredAgents ? deps.listDiscoveredAgents() : (0, cli_defaults_1.defaultListDiscoveredAgents)());
1272
1345
  if (discovered.length === 0 && deps.runSerpentGuide) {
1346
+ // Hatch-or-clone choice when promptInput is available
1347
+ if (deps.promptInput) {
1348
+ const choice = await deps.promptInput("No agents found. Would you like to hatch a new agent or clone an existing one? (hatch/clone): ");
1349
+ if (choice.trim().toLowerCase() === "clone") {
1350
+ (0, runtime_1.emitNervesEvent)({
1351
+ component: "daemon",
1352
+ event: "daemon.first_run_choice_clone",
1353
+ message: "user chose clone in first-run flow",
1354
+ meta: {},
1355
+ });
1356
+ const remote = await deps.promptInput("Enter the git remote URL for the agent bundle: ");
1357
+ // Run clone execution path
1358
+ const cloneCommand = { kind: "clone", remote: remote.trim() };
1359
+ return await runOuroCli(["clone", cloneCommand.remote], deps);
1360
+ }
1361
+ (0, runtime_1.emitNervesEvent)({
1362
+ component: "daemon",
1363
+ event: "daemon.first_run_choice_hatch",
1364
+ message: "user chose hatch in first-run flow",
1365
+ meta: {},
1366
+ });
1367
+ }
1273
1368
  // System setup first — ouro command, subagents, UTI — before the interactive specialist
1274
1369
  await performSystemSetup(deps);
1275
1370
  const hatchlingName = await deps.runSerpentGuide();
@@ -1497,6 +1592,11 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
1497
1592
  progress.startPhase("bundle cleanup");
1498
1593
  progress.completePhase("bundle cleanup", `pruned ${prunedBundles.length} stale bundle${prunedBundles.length === 1 ? "" : "s"}`);
1499
1594
  }
1595
+ // ── manual-clone detection: offer to enable sync for manually cloned bundles ──
1596
+ await checkManualCloneBundles({
1597
+ bundlesRoot: deps.bundlesRoot ?? bundlesRoot,
1598
+ promptInput: deps.promptInput,
1599
+ });
1500
1600
  progress.startPhase("starting daemon");
1501
1601
  const daemonResult = await ensureDaemonRunning({
1502
1602
  ...deps,
@@ -1908,17 +2008,57 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
1908
2008
  // ── setup: configure dev tool integration ──
1909
2009
  if (command.kind === "setup") {
1910
2010
  const { tool, agent: setupAgent } = command;
2011
+ const platform = (0, platform_1.detectPlatform)();
2012
+ // Windows native is not yet supported
2013
+ if (platform === "windows-native") {
2014
+ (0, runtime_1.emitNervesEvent)({
2015
+ component: "daemon",
2016
+ event: "daemon.setup_windows_native_unsupported",
2017
+ message: "Windows native setup not yet supported",
2018
+ meta: { tool, agent: setupAgent },
2019
+ });
2020
+ const message = "Windows native is not yet supported. Please run from WSL2: https://learn.microsoft.com/en-us/windows/wsl/install";
2021
+ deps.writeStdout(message);
2022
+ return message;
2023
+ }
2024
+ // Resolve platform-specific paths and commands
2025
+ let claudeCmd;
2026
+ let mcpServePrefix;
2027
+ let hookPrefix;
2028
+ let claudeConfigDir;
2029
+ if (platform === "wsl") {
2030
+ const winProfile = (0, child_process_1.execFileSync)("cmd.exe", ["/C", "echo", "%USERPROFILE%"], { stdio: "pipe" }).toString().trim();
2031
+ const windowsHome = (0, child_process_1.execFileSync)("wslpath", ["-u", winProfile], { stdio: "pipe" }).toString().trim();
2032
+ (0, runtime_1.emitNervesEvent)({
2033
+ component: "daemon",
2034
+ event: "daemon.setup_wsl_home_resolved",
2035
+ message: "resolved Windows home from WSL",
2036
+ meta: { windowsHome },
2037
+ });
2038
+ claudeCmd = "claude.exe";
2039
+ mcpServePrefix = "wsl ";
2040
+ hookPrefix = "wsl ";
2041
+ claudeConfigDir = path.join(windowsHome, ".claude");
2042
+ }
2043
+ else {
2044
+ // macos or linux
2045
+ claudeCmd = "claude";
2046
+ mcpServePrefix = "";
2047
+ hookPrefix = "";
2048
+ claudeConfigDir = path.join(os.homedir(), ".claude");
2049
+ }
1911
2050
  const sourceRoot = (0, identity_1.getRepoRoot)();
1912
2051
  const runtimeMode = (0, runtime_mode_1.detectRuntimeMode)(sourceRoot);
1913
- const mcpServeCommand = runtimeMode === "dev"
2052
+ const baseMcpServeCommand = runtimeMode === "dev"
1914
2053
  ? `node ${path.join(sourceRoot, "dist", "heart", "daemon", "ouro-bot-entry.js")} mcp-serve --agent ${setupAgent}`
1915
2054
  : `ouro mcp-serve --agent ${setupAgent}`;
2055
+ const mcpServeCommand = `${mcpServePrefix}${baseMcpServeCommand}`;
1916
2056
  if (tool === "claude-code") {
1917
2057
  // 1. Register MCP server with Claude Code
1918
- const mcpAddCmd = `claude mcp add ouro-${setupAgent} -s user -- ${mcpServeCommand}`;
2058
+ const mcpAddCmd = `${claudeCmd} mcp add ouro-${setupAgent} -s user -- ${mcpServeCommand}`;
1919
2059
  (0, child_process_1.execSync)(mcpAddCmd, { stdio: "pipe" });
1920
- // 2. Write hooks config to ~/.claude/settings.json
1921
- const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
2060
+ // 2. Write hooks config
2061
+ const settingsPath = path.join(claudeConfigDir, "settings.json");
1922
2062
  let settings = {};
1923
2063
  if (fs.existsSync(settingsPath)) {
1924
2064
  try {
@@ -1926,13 +2066,11 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
1926
2066
  }
1927
2067
  catch { /* start fresh */ }
1928
2068
  }
1929
- // Use `ouro hook <event>` — resolves the right code based on dev vs installed mode.
1930
- // Bare `ouro` works because ouro is on PATH via ~/.ouro-cli/bin/.
1931
2069
  settings.hooks = {
1932
2070
  ...(settings.hooks ?? {}),
1933
- SessionStart: [{ hooks: [{ type: "command", command: `ouro hook session-start --agent ${setupAgent}`, timeout: 5 }] }],
1934
- Stop: [{ hooks: [{ type: "command", command: `ouro hook stop --agent ${setupAgent}`, timeout: 5 }] }],
1935
- PostToolUse: [{ matcher: "Bash|Edit|Write", hooks: [{ type: "command", command: `ouro hook post-tool-use --agent ${setupAgent}`, timeout: 5 }] }],
2071
+ SessionStart: [{ hooks: [{ type: "command", command: `${hookPrefix}ouro hook session-start --agent ${setupAgent}`, timeout: 5 }] }],
2072
+ Stop: [{ hooks: [{ type: "command", command: `${hookPrefix}ouro hook stop --agent ${setupAgent}`, timeout: 5 }] }],
2073
+ PostToolUse: [{ matcher: "Bash|Edit|Write", hooks: [{ type: "command", command: `${hookPrefix}ouro hook post-tool-use --agent ${setupAgent}`, timeout: 5 }] }],
1936
2074
  };
1937
2075
  fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
1938
2076
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
@@ -1940,10 +2078,10 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
1940
2078
  component: "daemon",
1941
2079
  event: "daemon.setup_complete",
1942
2080
  message: "dev tool setup complete",
1943
- meta: { tool, agent: setupAgent, runtimeMode },
2081
+ meta: { tool, agent: setupAgent, runtimeMode, platform },
1944
2082
  });
1945
- // 3. Write conversation formatting instructions to ~/.claude/CLAUDE.md
1946
- const claudeMdPath = path.join(os.homedir(), ".claude", "CLAUDE.md");
2083
+ // 3. Write conversation formatting instructions
2084
+ const claudeMdPath = path.join(claudeConfigDir, "CLAUDE.md");
1947
2085
  const agentInstructions = `\n## Agent conversations (ouro)\nWhen using MCP \`send_message\` to talk to an ouro agent, format the exchange clearly:\n- Before the tool call, briefly say what you're asking/telling the agent\n- After the response, quote the agent's reply in a blockquote, then add your reaction\n- Example: **Me → Agent:** "question" / > **Agent:** "response" / Your synthesis here\n`;
1948
2086
  let existingClaudeMd = "";
1949
2087
  if (fs.existsSync(claudeMdPath)) {
@@ -1964,7 +2102,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
1964
2102
  component: "daemon",
1965
2103
  event: "daemon.setup_complete",
1966
2104
  message: "dev tool setup complete",
1967
- meta: { tool, agent: setupAgent, runtimeMode },
2105
+ meta: { tool, agent: setupAgent, runtimeMode, platform },
1968
2106
  });
1969
2107
  const message = `setup complete: codex + ${setupAgent}\n MCP server registered`;
1970
2108
  deps.writeStdout(message);
@@ -2616,6 +2754,65 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
2616
2754
  });
2617
2755
  return output;
2618
2756
  }
2757
+ // ── clone: clone an agent bundle from a git remote ──
2758
+ if (command.kind === "clone") {
2759
+ (0, runtime_1.emitNervesEvent)({
2760
+ component: "daemon",
2761
+ event: "daemon.clone_start",
2762
+ message: "starting agent bundle clone",
2763
+ meta: { remote: command.remote, agent: command.agent },
2764
+ });
2765
+ // 1. Check git is installed
2766
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_git_check", message: "checking git installation", meta: {} });
2767
+ try {
2768
+ (0, child_process_1.execFileSync)("git", ["--version"], { stdio: "pipe" });
2769
+ }
2770
+ catch (err) {
2771
+ const message = "git is not installed -- install it from https://git-scm.com\nOn macOS: brew install git\nOn Ubuntu/Debian: sudo apt install git\nOn Windows: download from https://git-scm.com/download/win";
2772
+ deps.writeStdout(message);
2773
+ return message;
2774
+ }
2775
+ // 2. Infer agent name
2776
+ const agentName = command.agent ?? (0, cli_parse_1.inferAgentNameFromRemote)(command.remote);
2777
+ const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
2778
+ const targetPath = path.join(bundlesRoot, agentName + ".ouro");
2779
+ // 3. Check target path does not exist
2780
+ if (fs.existsSync(targetPath)) {
2781
+ const message = `${targetPath} already exists. Remove it first or use --agent to pick a different name.`;
2782
+ deps.writeStdout(message);
2783
+ return message;
2784
+ }
2785
+ // 4. Check remote accessible
2786
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_remote_check", message: "checking remote accessibility", meta: { remote: command.remote } });
2787
+ try {
2788
+ (0, child_process_1.execFileSync)("git", ["ls-remote", "--exit-code", command.remote], { stdio: "pipe", timeout: 15000 });
2789
+ }
2790
+ catch {
2791
+ const message = `could not reach remote: ${command.remote}\nCheck the URL and your network connection.`;
2792
+ deps.writeStdout(message);
2793
+ return message;
2794
+ }
2795
+ // 5. Clone
2796
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_git_clone", message: "cloning agent bundle", meta: { remote: command.remote, targetPath } });
2797
+ (0, child_process_1.execFileSync)("git", ["clone", command.remote, targetPath], { stdio: "pipe" });
2798
+ // 6. Create machine identity
2799
+ (0, machine_identity_1.loadOrCreateMachineIdentity)();
2800
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_identity_created", message: "machine identity created", meta: {} });
2801
+ // 7. Enable sync in agent.json
2802
+ const agentJsonPath = path.join(targetPath, "agent.json");
2803
+ if (fs.existsSync(agentJsonPath)) {
2804
+ const raw = fs.readFileSync(agentJsonPath, "utf-8");
2805
+ const config = JSON.parse(raw);
2806
+ config.sync = { enabled: true, remote: "origin" };
2807
+ fs.writeFileSync(agentJsonPath, JSON.stringify(config, null, 2) + "\n");
2808
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_sync_enabled", message: "sync enabled in agent.json", meta: { agentName } });
2809
+ }
2810
+ // 8. Output success message
2811
+ (0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_complete", message: "clone complete", meta: { agentName, targetPath } });
2812
+ const message = `cloned ${agentName} to ${targetPath}\nsync enabled (remote: origin)\nnext steps:\n ouro auth run --agent ${agentName}`;
2813
+ deps.writeStdout(message);
2814
+ return message;
2815
+ }
2619
2816
  const daemonCommand = toDaemonCommand(command);
2620
2817
  let response;
2621
2818
  try {
@@ -67,6 +67,12 @@ exports.COMMAND_REGISTRY = {
67
67
  usage: "ouro versions",
68
68
  example: "ouro versions",
69
69
  },
70
+ clone: {
71
+ category: "Lifecycle",
72
+ description: "Clone an existing agent bundle from a git remote onto this machine",
73
+ usage: "ouro clone <remote> [--agent <name>]",
74
+ example: "ouro clone https://github.com/user/myagent.ouro.git",
75
+ },
70
76
  doctor: {
71
77
  category: "Lifecycle",
72
78
  description: "Run diagnostic checks on the ouro installation",
@@ -11,6 +11,7 @@ exports.extractFacingFlag = extractFacingFlag;
11
11
  exports.facingToProviderLane = facingToProviderLane;
12
12
  exports.isAgentProvider = isAgentProvider;
13
13
  exports.usage = usage;
14
+ exports.inferAgentNameFromRemote = inferAgentNameFromRemote;
14
15
  exports.parseMcpServeCommand = parseMcpServeCommand;
15
16
  exports.parseOuroCommand = parseOuroCommand;
16
17
  const types_1 = require("../../mind/friends/types");
@@ -100,6 +101,7 @@ function usage() {
100
101
  " ouro mcp call <server> <tool> [--args '{...}']",
101
102
  " ouro rollback [<version>]",
102
103
  " ouro versions",
104
+ " ouro clone <remote> [--agent <name>]",
103
105
  " ouro doctor",
104
106
  ].join("\n");
105
107
  }
@@ -674,6 +676,41 @@ function parseMcpCommand(args) {
674
676
  }
675
677
  throw new Error(`Usage\n${usage()}`);
676
678
  }
679
+ function inferAgentNameFromRemote(remote) {
680
+ // Remove trailing slash
681
+ let name = remote.replace(/\/+$/, "");
682
+ // Handle SSH URLs (git@host:user/repo) — extract after last / or :
683
+ const lastSlash = name.lastIndexOf("/");
684
+ const lastColon = name.lastIndexOf(":");
685
+ const lastSep = Math.max(lastSlash, lastColon);
686
+ if (lastSep !== -1) {
687
+ name = name.slice(lastSep + 1);
688
+ }
689
+ // Strip .git suffix
690
+ name = name.replace(/\.git$/, "");
691
+ // Strip .ouro suffix
692
+ name = name.replace(/\.ouro$/, "");
693
+ return name;
694
+ }
695
+ function parseCloneCommand(args) {
696
+ let remote;
697
+ let agent;
698
+ for (let i = 0; i < args.length; i++) {
699
+ if (args[i] === "--agent" && args[i + 1]) {
700
+ agent = args[++i];
701
+ continue;
702
+ }
703
+ if (!remote) {
704
+ remote = args[i];
705
+ }
706
+ }
707
+ if (!remote) {
708
+ throw new Error("clone requires a remote URL.\nUsage: ouro clone <remote> [--agent <name>]");
709
+ }
710
+ return agent
711
+ ? { kind: "clone", remote, agent }
712
+ : { kind: "clone", remote };
713
+ }
677
714
  function parseMcpServeCommand(args) {
678
715
  let agent;
679
716
  let friendId;
@@ -903,6 +940,8 @@ function parseOuroCommand(args) {
903
940
  return parseMcpServeCommand(args.slice(1));
904
941
  if (head === "setup")
905
942
  return parseSetupCommand(args.slice(1));
943
+ if (head === "clone")
944
+ return parseCloneCommand(args.slice(1));
906
945
  if (head === "doctor")
907
946
  return { kind: "doctor" };
908
947
  if (head === "bluebubbles")
@@ -0,0 +1,81 @@
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.detectPlatform = detectPlatform;
37
+ const fs = __importStar(require("fs"));
38
+ const runtime_1 = require("../nerves/runtime");
39
+ function detectPlatform(deps = {}) {
40
+ const platform = deps.platform ?? process.platform;
41
+ const env = deps.env ?? process.env;
42
+ const readFile = deps.readFileSync ?? ((p) => fs.readFileSync(p, "utf-8"));
43
+ let result;
44
+ if (platform === "darwin") {
45
+ result = "macos";
46
+ }
47
+ else if (platform === "win32") {
48
+ result = "windows-native";
49
+ }
50
+ else if (platform === "linux") {
51
+ result = detectLinuxOrWsl(env, readFile);
52
+ }
53
+ else {
54
+ // Unknown platform — treat as linux
55
+ result = "linux";
56
+ }
57
+ (0, runtime_1.emitNervesEvent)({
58
+ component: "daemon",
59
+ event: "daemon.platform_detected",
60
+ message: "detected platform",
61
+ meta: { platform, result },
62
+ });
63
+ return result;
64
+ }
65
+ function detectLinuxOrWsl(env, readFile) {
66
+ // Primary: WSL_DISTRO_NAME env var
67
+ if (env.WSL_DISTRO_NAME && env.WSL_DISTRO_NAME.length > 0) {
68
+ return "wsl";
69
+ }
70
+ // Fallback: /proc/version containing "microsoft" (case-insensitive)
71
+ try {
72
+ const procVersion = readFile("/proc/version");
73
+ if (/microsoft/i.test(procVersion)) {
74
+ return "wsl";
75
+ }
76
+ }
77
+ catch {
78
+ // /proc/version not readable — not WSL
79
+ }
80
+ return "linux";
81
+ }
@@ -251,12 +251,28 @@ async function streamAnthropicMessages(client, model, request) {
251
251
  // prompt when using OAuth setup tokens (sk-ant-oat01). Without it, Opus/Sonnet
252
252
  // 4.6 requests are rejected with 400. This is the API's validation that the
253
253
  // token is being used by a Claude Code client.
254
- const claudeCodePreamble = { type: "text", text: "You are Claude Code, Anthropic's official CLI for Claude." };
255
- if (system) {
256
- params.system = [claudeCodePreamble, { type: "text", text: system }];
254
+ const preambleText = "You are Claude Code, Anthropic's official CLI for Claude.";
255
+ if (request.systemPrompt) {
256
+ // Structured SystemPrompt: merge preamble + stable prefix into one cached block,
257
+ // volatile suffix as a separate uncached block.
258
+ const stableBlock = {
259
+ type: "text",
260
+ text: preambleText + "\n\n" + request.systemPrompt.stable,
261
+ cache_control: { type: "ephemeral" },
262
+ };
263
+ if (request.systemPrompt.volatile) {
264
+ params.system = [stableBlock, { type: "text", text: request.systemPrompt.volatile }];
265
+ }
266
+ else {
267
+ params.system = [stableBlock];
268
+ }
269
+ }
270
+ else if (system) {
271
+ // Fallback: no structured prompt, extract from messages (legacy path)
272
+ params.system = [{ type: "text", text: preambleText }, { type: "text", text: system }];
257
273
  }
258
274
  else {
259
- params.system = [claudeCodePreamble];
275
+ params.system = [{ type: "text", text: preambleText }];
260
276
  }
261
277
  if (anthropicTools.length > 0)
262
278
  params.tools = anthropicTools;
@@ -5,11 +5,12 @@ const prompt_1 = require("./prompt");
5
5
  const runtime_1 = require("../nerves/runtime");
6
6
  async function refreshSystemPrompt(messages, channel, options, context) {
7
7
  const newSystem = await (0, prompt_1.buildSystem)(channel, options, context);
8
+ const flattened = (0, prompt_1.flattenSystemPrompt)(newSystem);
8
9
  if (messages.length > 0 && messages[0].role === "system") {
9
- messages[0] = { role: "system", content: newSystem };
10
+ messages[0] = { role: "system", content: flattened };
10
11
  }
11
12
  else {
12
- messages.unshift({ role: "system", content: newSystem });
13
+ messages.unshift({ role: "system", content: flattened });
13
14
  }
14
15
  (0, runtime_1.emitNervesEvent)({
15
16
  event: "mind.system_prompt_refreshed",
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.flattenSystemPrompt = flattenSystemPrompt;
36
37
  exports.resetPsycheCache = resetPsycheCache;
37
38
  exports.buildSessionSummary = buildSessionSummary;
38
39
  exports.bodyMapSection = bodyMapSection;
@@ -79,6 +80,10 @@ const daemon_health_1 = require("../heart/daemon/daemon-health");
79
80
  const scrutiny_1 = require("./scrutiny");
80
81
  const pulse_1 = require("../heart/daemon/pulse");
81
82
  const provider_visibility_1 = require("../heart/provider-visibility");
83
+ function flattenSystemPrompt(sp) {
84
+ const parts = [sp.stable, sp.volatile].filter(Boolean);
85
+ return parts.join("\n\n");
86
+ }
82
87
  // Lazy-loaded psyche text cache
83
88
  let _psycheCache = null;
84
89
  let _senseStatusLinesCache = null;
@@ -1256,7 +1261,7 @@ async function buildSystem(channel = "cli", options, context) {
1256
1261
  });
1257
1262
  // Backfill bundle-meta.json for existing agents that don't have one
1258
1263
  (0, bundle_manifest_1.backfillBundleMeta)((0, identity_1.getAgentRoot)());
1259
- const system = [
1264
+ const stableParts = [
1260
1265
  // Group 1: who i am
1261
1266
  "# who i am",
1262
1267
  soulSection(),
@@ -1264,14 +1269,12 @@ async function buildSystem(channel = "cli", options, context) {
1264
1269
  loreSection(),
1265
1270
  tacitKnowledgeSection(),
1266
1271
  aspirationsSection(),
1267
- // Group 2: my body & environment
1272
+ // Group 2: my body & environment (minus dateSection and rhythmStatusSection)
1268
1273
  "# my body & environment",
1269
1274
  bodyMapSection((0, identity_1.getAgentName)(), channel),
1270
1275
  runtimeInfoSection(channel, options),
1271
- rhythmStatusSection(options?.daemonHealth),
1272
1276
  channelNatureSection((0, channel_1.getChannelCapabilities)(channel)),
1273
1277
  providerSection(channel, options),
1274
- dateSection(),
1275
1278
  // Group 3: my tools & capabilities
1276
1279
  "# my tools & capabilities",
1277
1280
  toolsSection(channel, options, context),
@@ -1302,6 +1305,11 @@ async function buildSystem(channel = "cli", options, context) {
1302
1305
  groupChatParticipationSection(context),
1303
1306
  feedbackSignalSection(context),
1304
1307
  ] : []),
1308
+ ];
1309
+ const volatileParts = [
1310
+ // Volatile sections from Group 2 (date and rhythm change every turn)
1311
+ dateSection(),
1312
+ rhythmStatusSection(options?.daemonHealth),
1305
1313
  // Group 7: dynamic state for this turn
1306
1314
  "# dynamic state for this turn",
1307
1315
  startOfTurnPacketSection(options),
@@ -1331,14 +1339,16 @@ async function buildSystem(channel = "cli", options, context) {
1331
1339
  // Group 9: task context
1332
1340
  "# task context",
1333
1341
  taskBoardSection(),
1334
- ]
1335
- .filter(Boolean)
1336
- .join("\n\n");
1342
+ ];
1343
+ const result = {
1344
+ stable: stableParts.filter(Boolean).join("\n\n"),
1345
+ volatile: volatileParts.filter(Boolean).join("\n\n"),
1346
+ };
1337
1347
  (0, runtime_1.emitNervesEvent)({
1338
1348
  event: "mind.step_end",
1339
1349
  component: "mind",
1340
1350
  message: "buildSystem completed",
1341
1351
  meta: { channel },
1342
1352
  });
1343
- return system;
1353
+ return result;
1344
1354
  }
@@ -646,7 +646,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
646
646
  const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
647
647
  const sessionMessages = existing?.messages && existing.messages.length > 0
648
648
  ? existing.messages
649
- : [{ role: "system", content: await resolvedDeps.buildSystem("bluebubbles", {}, context) }];
649
+ : [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await resolvedDeps.buildSystem("bluebubbles", {}, context)) }];
650
650
  if (event.kind === "message") {
651
651
  const agentName = resolvedDeps.getAgentName();
652
652
  if ((0, inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, event.chat.sessionKey, event.messageGuid)) {
@@ -511,7 +511,7 @@ async function runCliSession(options) {
511
511
  (0, commands_1.registerDefaultCommands)(registry);
512
512
  }
513
513
  const messages = options.messages
514
- ?? [{ role: "system", content: await (0, prompt_1.buildSystem)("cli") }];
514
+ ?? [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)("cli")) }];
515
515
  // ─── Rendering: TUI (Ink + Static) for TTY, imperative for tests/pipes ───
516
516
  const useTui = !options._testInputSource && process.stdin.isTTY === true;
517
517
  let currentAbort = null;
@@ -976,7 +976,7 @@ async function main(agentName, options) {
976
976
  const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
977
977
  const sessionMessages = existing?.messages && existing.messages.length > 0
978
978
  ? existing.messages
979
- : [{ role: "system", content: await (0, prompt_1.buildSystem)("cli", {}, resolvedContext) }];
979
+ : [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)("cli", {}, resolvedContext)) }];
980
980
  // Repair any orphaned tool calls from a crash mid-turn
981
981
  (0, core_1.repairOrphanedToolCalls)(sessionMessages);
982
982
  // Per-turn pipeline input: CLI capabilities and pending dir
@@ -672,7 +672,7 @@ async function runInnerDialogTurn(options) {
672
672
  // Fresh session: build system prompt
673
673
  const systemPrompt = await (0, prompt_1.buildSystem)("inner", { toolChoiceRequired: true });
674
674
  return {
675
- messages: [{ role: "system", content: systemPrompt }],
675
+ messages: [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(systemPrompt) }],
676
676
  sessionPath: sessionFilePath,
677
677
  };
678
678
  },
@@ -112,7 +112,7 @@ async function runSenseTurn(options) {
112
112
  let persistPromise;
113
113
  const sessionMessages = existing?.messages && existing.messages.length > 0
114
114
  ? existing.messages
115
- : [{ role: "system", content: await (0, prompt_1.buildSystem)(channel, {}, undefined) }];
115
+ : [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)(channel, {}, undefined)) }];
116
116
  // Pending dir
117
117
  const pendingDir = (0, pending_1.getPendingDir)(agentName, friendId, channel, sessionKey);
118
118
  // Accumulate response text via callbacks
@@ -655,7 +655,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
655
655
  const existing = (0, context_1.loadSession)(sessPath);
656
656
  const messages = existing?.messages && existing.messages.length > 0
657
657
  ? existing.messages
658
- : [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", {}, resolvedContext) }];
658
+ : [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)("teams", {}, resolvedContext)) }];
659
659
  (0, core_1.repairOrphanedToolCalls)(messages);
660
660
  return {
661
661
  messages,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.361",
3
+ "version": "0.1.0-alpha.362",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",