@ouro.bot/cli 0.1.0-alpha.174 → 0.1.0-alpha.176

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.
Files changed (35) hide show
  1. package/changelog.json +9 -0
  2. package/dist/heart/active-work.js +2 -2
  3. package/dist/heart/core.js +13 -2
  4. package/dist/heart/daemon/daemon-cli.js +17 -14
  5. package/dist/heart/daemon/daemon-entry.js +17 -0
  6. package/dist/heart/daemon/daemon.js +5 -2
  7. package/dist/heart/daemon/launchd.js +8 -4
  8. package/dist/mind/file-state.js +179 -0
  9. package/dist/mind/prompt.js +119 -60
  10. package/dist/mind/scrutiny.js +173 -0
  11. package/dist/nerves/coverage/audit.js +1 -1
  12. package/dist/repertoire/ado-semantic.js +5 -5
  13. package/dist/repertoire/coding/tools.js +35 -5
  14. package/dist/repertoire/shell-sessions.js +133 -0
  15. package/dist/repertoire/tools-base.js +145 -16
  16. package/dist/repertoire/tools.js +37 -0
  17. package/dist/senses/cli/adapter.js +307 -0
  18. package/dist/senses/cli/ink-app.js +156 -0
  19. package/dist/senses/cli/inline-diff.js +68 -0
  20. package/dist/senses/cli/message-list.js +84 -0
  21. package/dist/senses/cli/ouro-tui.js +296 -0
  22. package/dist/senses/cli/spinner-imperative.js +135 -0
  23. package/dist/senses/cli/spinner.js +101 -0
  24. package/dist/senses/cli/status-line.js +64 -0
  25. package/dist/senses/cli/streaming-markdown.js +486 -0
  26. package/dist/senses/cli/tool-display.js +77 -0
  27. package/dist/senses/cli/tool-render.js +89 -0
  28. package/dist/senses/cli/tui-store.js +198 -0
  29. package/dist/senses/cli/virtual-list.js +68 -0
  30. package/dist/senses/cli.js +231 -157
  31. package/dist/senses/pipeline.js +53 -10
  32. package/package.json +1 -1
  33. package/dist/outlook-ui/assets/index-DN6vVYPm.css +0 -1
  34. package/dist/outlook-ui/assets/index-M1l1tDTX.js +0 -61
  35. package/dist/outlook-ui/index.html +0 -15
@@ -40,8 +40,11 @@ const fg = __importStar(require("fast-glob"));
40
40
  const child_process_1 = require("child_process");
41
41
  const path = __importStar(require("path"));
42
42
  const skills_1 = require("./skills");
43
+ const shell_sessions_1 = require("./shell-sessions");
43
44
  const config_1 = require("../heart/config");
44
45
  const runtime_1 = require("../nerves/runtime");
46
+ const file_state_1 = require("../mind/file-state");
47
+ const scrutiny_1 = require("../mind/scrutiny");
45
48
  const identity_1 = require("../heart/identity");
46
49
  const identity_2 = require("../heart/identity");
47
50
  const socket_client_1 = require("../heart/daemon/socket-client");
@@ -337,7 +340,7 @@ exports.baseToolDefinitions = [
337
340
  type: "function",
338
341
  function: {
339
342
  name: "read_file",
340
- description: "read file contents",
343
+ description: "Read file contents. Results include line numbers. Use offset/limit for large files -- don't read the whole thing if you only need a section. Use this tool before editing any file. When reading code, read enough context to understand the surrounding logic, not just the target line.",
341
344
  parameters: {
342
345
  type: "object",
343
346
  properties: {
@@ -355,6 +358,17 @@ exports.baseToolDefinitions = [
355
358
  exports.editFileReadTracker.add(resolvedPath);
356
359
  const offset = a.offset ? parseInt(a.offset, 10) : undefined;
357
360
  const limit = a.limit ? parseInt(a.limit, 10) : undefined;
361
+ // Record in file state cache for staleness detection
362
+ try {
363
+ const mtime = fs.statSync(resolvedPath).mtimeMs;
364
+ const readContent = (offset === undefined && limit === undefined)
365
+ ? content
366
+ : content.split("\n").slice(offset ? offset - 1 : 0, limit !== undefined ? (offset ? offset - 1 : 0) + limit : undefined).join("\n");
367
+ file_state_1.fileStateCache.record(resolvedPath, readContent, mtime, offset, limit);
368
+ }
369
+ catch {
370
+ // stat failed -- skip cache recording
371
+ }
358
372
  if (offset === undefined && limit === undefined)
359
373
  return content;
360
374
  const lines = content.split("\n");
@@ -369,7 +383,7 @@ exports.baseToolDefinitions = [
369
383
  type: "function",
370
384
  function: {
371
385
  name: "write_file",
372
- description: "write content to file",
386
+ description: "Prefer this tool for creating new files or fully replacing existing ones. You MUST read an existing file with read_file before overwriting it. Prefer edit_file for modifying existing files -- it only sends the diff. Do not create documentation files (*.md, README) by default; only do so when explicitly asked or when documentation is clearly part of the requested change.",
373
387
  parameters: {
374
388
  type: "object",
375
389
  properties: { path: { type: "string" }, content: { type: "string" } },
@@ -381,7 +395,10 @@ exports.baseToolDefinitions = [
381
395
  const resolvedPath = resolveLocalToolPath(a.path);
382
396
  fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
383
397
  fs.writeFileSync(resolvedPath, a.content, "utf-8");
384
- return "ok";
398
+ (0, scrutiny_1.trackModifiedFile)(resolvedPath);
399
+ const scrutiny = (0, scrutiny_1.getPostImplementationScrutiny)((0, scrutiny_1.getModifiedFileCount)());
400
+ /* v8 ignore next -- scrutiny appendix branch depends on session-level file count @preserve */
401
+ return scrutiny ? `ok\n\n${scrutiny}` : "ok";
385
402
  },
386
403
  summaryKeys: ["path"],
387
404
  },
@@ -390,7 +407,7 @@ exports.baseToolDefinitions = [
390
407
  type: "function",
391
408
  function: {
392
409
  name: "edit_file",
393
- description: "surgically edit a file by replacing an exact string. the file must have been read via read_file first. old_string must match exactly one location in the file.",
410
+ description: "Surgically edit a file by replacing an exact string. The file MUST have been read via read_file first -- this tool will reject the call otherwise. old_string must match EXACTLY ONE location in the file -- if it matches zero or multiple, the edit fails. To fix: provide more surrounding context to make the match unique. Preserve exact indentation (tabs/spaces) from the file. Prefer this over write_file for modifications -- it only sends the diff.",
394
411
  parameters: {
395
412
  type: "object",
396
413
  properties: {
@@ -407,6 +424,8 @@ exports.baseToolDefinitions = [
407
424
  if (!exports.editFileReadTracker.has(resolvedPath)) {
408
425
  return `error: you must read the file with read_file before editing it. call read_file on ${a.path} first.`;
409
426
  }
427
+ // Check staleness before editing
428
+ const stalenessCheck = file_state_1.fileStateCache.isStale(resolvedPath);
410
429
  let content;
411
430
  try {
412
431
  content = fs.readFileSync(resolvedPath, "utf-8");
@@ -434,13 +453,33 @@ exports.baseToolDefinitions = [
434
453
  const idx = occurrences[0];
435
454
  const updated = content.slice(0, idx) + a.new_string + content.slice(idx + a.old_string.length);
436
455
  fs.writeFileSync(resolvedPath, updated, "utf-8");
456
+ // Update file state cache with new content
457
+ try {
458
+ const newMtime = fs.statSync(resolvedPath).mtimeMs;
459
+ file_state_1.fileStateCache.record(resolvedPath, updated, newMtime);
460
+ }
461
+ catch {
462
+ // stat failed -- skip cache update
463
+ }
437
464
  // Build contextual diff
438
465
  const lines = updated.split("\n");
439
466
  const prefixLines = content.slice(0, idx).split("\n");
440
467
  const changeStartLine = prefixLines.length - 1;
441
468
  const newStringLines = a.new_string.split("\n");
442
469
  const changeEndLine = changeStartLine + newStringLines.length;
443
- return buildContextDiff(lines, changeStartLine, changeEndLine);
470
+ const diffResult = buildContextDiff(lines, changeStartLine, changeEndLine);
471
+ // Track modified file and compute scrutiny appendix
472
+ (0, scrutiny_1.trackModifiedFile)(resolvedPath);
473
+ const scrutiny = (0, scrutiny_1.getPostImplementationScrutiny)((0, scrutiny_1.getModifiedFileCount)());
474
+ // Append staleness warning if detected (do not block -- TTFA)
475
+ /* v8 ignore start -- staleness+diff+scrutiny combo not exercised in integration tests @preserve */
476
+ if (stalenessCheck.stale) {
477
+ const base = `${diffResult}\n\n⚠️ warning: file changed externally since last read -- re-read recommended`;
478
+ return scrutiny ? `${base}\n\n${scrutiny}` : base;
479
+ }
480
+ /* v8 ignore stop */
481
+ /* v8 ignore next -- scrutiny appendix branch depends on session-level file count @preserve */
482
+ return scrutiny ? `${diffResult}\n\n${scrutiny}` : diffResult;
444
483
  },
445
484
  summaryKeys: ["path"],
446
485
  },
@@ -449,7 +488,7 @@ exports.baseToolDefinitions = [
449
488
  type: "function",
450
489
  function: {
451
490
  name: "glob",
452
- description: "find files matching a glob pattern. returns matching paths sorted alphabetically, one per line.",
491
+ description: "Find files matching a glob pattern, sorted alphabetically. Use this instead of shell commands like `find` or `ls`. For broad exploratory searches that would require multiple rounds of globbing and grepping, consider using claude or coding_spawn.",
453
492
  parameters: {
454
493
  type: "object",
455
494
  properties: {
@@ -472,7 +511,7 @@ exports.baseToolDefinitions = [
472
511
  type: "function",
473
512
  function: {
474
513
  name: "grep",
475
- description: "search file contents for lines matching a regex pattern. searches recursively when given a directory. returns matching lines with file path and line numbers.",
514
+ description: "Search file contents for lines matching a regex pattern. Searches recursively in directories. Use this instead of shell commands like `grep` or `rg`. Returns matching lines with file path and line numbers. Use context_lines for surrounding context. Use include to filter file types (e.g., '*.ts').",
476
515
  parameters: {
477
516
  type: "object",
478
517
  properties: {
@@ -581,22 +620,112 @@ exports.baseToolDefinitions = [
581
620
  type: "function",
582
621
  function: {
583
622
  name: "shell",
584
- description: "run shell command",
623
+ description: "Run a shell command and return stdout/stderr. Working directory persists between calls. Use dedicated tools instead of shell when available: read_file instead of cat, edit_file instead of sed, glob instead of find, grep instead of grep/rg. Reserve shell for operations that genuinely need the shell: installing packages, running builds/tests, git operations, process management. Be careful with destructive commands -- consider reversibility before running. If a command fails, read the error output before retrying with a different approach.",
585
624
  parameters: {
586
625
  type: "object",
587
- properties: { command: { type: "string" } },
626
+ properties: {
627
+ command: { type: "string" },
628
+ timeout_ms: {
629
+ type: "number",
630
+ description: "Timeout in milliseconds. Default: 30000. Max: 600000.",
631
+ },
632
+ background: {
633
+ type: "boolean",
634
+ description: "Run in background. Returns immediately with a process ID. Use shell_status/shell_tail to monitor.",
635
+ },
636
+ },
588
637
  required: ["command"],
589
638
  },
590
639
  },
591
640
  },
592
641
  handler: (a) => {
593
- return (0, child_process_1.execSync)(a.command, {
642
+ // Destructive pattern detection (friction, not a block)
643
+ const destructivePatterns = (0, shell_sessions_1.detectDestructivePatterns)(a.command);
644
+ if (destructivePatterns.length > 0) {
645
+ (0, runtime_1.emitNervesEvent)({
646
+ level: "warn",
647
+ event: "tool.shell.destructive_detected",
648
+ component: "tools",
649
+ message: `destructive pattern detected: ${destructivePatterns.join(", ")}`,
650
+ meta: { command: a.command, patterns: destructivePatterns },
651
+ });
652
+ }
653
+ // Background mode: spawn and return immediately
654
+ if (a.background === "true") {
655
+ const session = (0, shell_sessions_1.spawnBackgroundShell)(a.command);
656
+ return JSON.stringify({ id: session.id, command: session.command, status: session.status });
657
+ }
658
+ const MAX_TIMEOUT = 600000;
659
+ const requestedTimeout = Number(a.timeout_ms) || 0;
660
+ let configDefault = 30000;
661
+ try {
662
+ configDefault = (0, identity_1.loadAgentConfig)().shell?.defaultTimeout ?? 30000;
663
+ }
664
+ catch { /* test env: no --agent flag */ }
665
+ const baseTimeout = requestedTimeout > 0 ? requestedTimeout : configDefault;
666
+ const timeout = Math.min(baseTimeout, MAX_TIMEOUT);
667
+ const output = (0, child_process_1.execSync)(a.command, {
594
668
  encoding: "utf-8",
595
- timeout: 30000,
669
+ timeout,
596
670
  });
671
+ if (destructivePatterns.length > 0) {
672
+ return `${output}\n\n--- destructive pattern detected: ${destructivePatterns.join(", ")} ---`;
673
+ }
674
+ return output;
597
675
  },
598
676
  summaryKeys: ["command"],
599
677
  },
678
+ {
679
+ tool: {
680
+ type: "function",
681
+ function: {
682
+ name: "shell_status",
683
+ description: "Check status of background shell processes. Omit id to list all.",
684
+ parameters: {
685
+ type: "object",
686
+ properties: {
687
+ id: { type: "string", description: "Background shell process ID" },
688
+ },
689
+ },
690
+ },
691
+ },
692
+ handler: (a) => {
693
+ if (!a.id) {
694
+ return JSON.stringify((0, shell_sessions_1.listShellSessions)());
695
+ }
696
+ const session = (0, shell_sessions_1.getShellSession)(a.id);
697
+ if (!session)
698
+ return `process not found: ${a.id}`;
699
+ return JSON.stringify(session);
700
+ },
701
+ summaryKeys: ["id"],
702
+ },
703
+ {
704
+ tool: {
705
+ type: "function",
706
+ function: {
707
+ name: "shell_tail",
708
+ description: "Show recent output from a background shell process.",
709
+ parameters: {
710
+ type: "object",
711
+ properties: {
712
+ id: { type: "string", description: "Background shell process ID" },
713
+ },
714
+ required: ["id"],
715
+ },
716
+ },
717
+ },
718
+ handler: (a) => {
719
+ /* v8 ignore next -- schema requires id, defensive guard @preserve */
720
+ if (!a.id)
721
+ return "id is required";
722
+ const output = (0, shell_sessions_1.tailShellSession)(a.id);
723
+ if (output === undefined)
724
+ return `process not found: ${a.id}`;
725
+ return output || "(no output yet)";
726
+ },
727
+ summaryKeys: ["id"],
728
+ },
600
729
  {
601
730
  tool: {
602
731
  type: "function",
@@ -636,7 +765,7 @@ exports.baseToolDefinitions = [
636
765
  type: "function",
637
766
  function: {
638
767
  name: "claude",
639
- description: "use claude code to query this codebase or get an outside perspective. useful for code review, second opinions, and asking questions about your own source.",
768
+ description: "use claude code to query this codebase or get an outside perspective. Use for code review, second opinions, or questions that benefit from a fresh perspective outside this conversation's context.",
640
769
  parameters: {
641
770
  type: "object",
642
771
  properties: { prompt: { type: "string" } },
@@ -709,7 +838,7 @@ exports.baseToolDefinitions = [
709
838
  type: "function",
710
839
  function: {
711
840
  name: "recall",
712
- description: "recall what i know — search my diary and journal for facts, thoughts, and working notes that match a query",
841
+ description: "Search my diary and journal for facts, thoughts, and working notes matching a query. Uses semantic similarity -- phrasing matters. Try different angles if the first query doesn't find what you're looking for. Check recall before asking the human something you might already know.",
713
842
  parameters: {
714
843
  type: "object",
715
844
  properties: { query: { type: "string" } },
@@ -762,7 +891,7 @@ exports.baseToolDefinitions = [
762
891
  type: "function",
763
892
  function: {
764
893
  name: "diary_write",
765
- description: "write an entry in my diary — something i learned, noticed, or concluded that i want to recall later. optional 'about' tags the entry to a person, topic, or context.",
894
+ description: "Write an entry in my diary -- something I learned, noticed, or concluded that I want to recall later. Use 'about' to tag the entry to a person, topic, or context. Write for my future self: include enough context that the entry makes sense without the surrounding conversation. Prefer durable conclusions over passing noise. Don't duplicate what already belongs in friend notes.",
766
895
  parameters: {
767
896
  type: "object",
768
897
  properties: {
@@ -1376,7 +1505,7 @@ exports.ponderTool = {
1376
1505
  type: "function",
1377
1506
  function: {
1378
1507
  name: "ponder",
1379
- description: "i need to sit with this. from a conversation, takes the thread inward with a thought and a parting word. from inner dialog, keeps the wheel turning for another pass. must be the only tool call in the turn.",
1508
+ description: "i need to sit with this. from a conversation, takes the thread inward with a thought and a parting word. from inner dialog, keeps the wheel turning for another pass. must be the only tool call in the turn. Use when a question deserves more thought than this turn allows. Don't ponder trivial questions.",
1380
1509
  parameters: {
1381
1510
  type: "object",
1382
1511
  properties: {
@@ -1409,7 +1538,7 @@ exports.settleTool = {
1409
1538
  type: "function",
1410
1539
  function: {
1411
1540
  name: "settle",
1412
- description: "respond to the user with your message. call this tool when you are ready to deliver your response.",
1541
+ description: "respond to the user with your message. call this tool when you are ready to deliver your response. Only call when you have a substantive response. If you're settling with 'I'll look into that,' you probably should be using a tool instead.",
1413
1542
  parameters: {
1414
1543
  type: "object",
1415
1544
  properties: {
@@ -38,6 +38,7 @@ exports.getToolsForChannel = getToolsForChannel;
38
38
  exports.isConfirmationRequired = isConfirmationRequired;
39
39
  exports.execTool = execTool;
40
40
  exports.summarizeArgs = summarizeArgs;
41
+ exports.buildToolResultSummary = buildToolResultSummary;
41
42
  const tools_base_1 = require("./tools-base");
42
43
  const tools_teams_1 = require("./tools-teams");
43
44
  const tools_bluebubbles_1 = require("./tools-bluebubbles");
@@ -387,3 +388,39 @@ function summarizeArgs(name, args) {
387
388
  }
388
389
  return summarizeUnknownArgs(args);
389
390
  }
391
+ /**
392
+ * Build an enriched tool result summary for display on remote channels.
393
+ * For recognized tools, includes result-derived info (diff stats, exit codes, etc.).
394
+ * Falls back to arg-based summary for unrecognized tools.
395
+ */
396
+ function buildToolResultSummary(name, args, _result, success) {
397
+ switch (name) {
398
+ case "edit_file": {
399
+ if (!success)
400
+ return summarizeArgs(name, args);
401
+ const addedLines = (args.new_string ?? "").split("\n").length;
402
+ const removedLines = (args.old_string ?? "").split("\n").length;
403
+ return `+${addedLines} -${removedLines} lines in ${args.path ?? "unknown"}`;
404
+ }
405
+ case "shell": {
406
+ const cmd = args.command ?? "?";
407
+ const exitCode = success ? 0 : 1;
408
+ return `$ ${cmd} (exit ${exitCode})`;
409
+ }
410
+ case "read_file":
411
+ return `path=${args.path ?? "unknown"}`;
412
+ case "write_file":
413
+ return `path=${args.path ?? "unknown"}`;
414
+ case "glob":
415
+ return `pattern=${args.pattern ?? "?"} ${args.cwd ? `cwd=${args.cwd}` : ""}`.trim();
416
+ case "grep":
417
+ return `pattern=${args.pattern ?? "?"} ${args.path ? `path=${args.path}` : ""}`.trim();
418
+ case "coding_spawn": {
419
+ const taskRef = args.taskRef ?? "unknown";
420
+ const status = success ? "spawned" : "failed";
421
+ return `${taskRef} -> ${status}`;
422
+ }
423
+ default:
424
+ return summarizeArgs(name, args);
425
+ }
426
+ }
@@ -0,0 +1,307 @@
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.CliStore = void 0;
37
+ exports.createInkCallbacks = createInkCallbacks;
38
+ exports.InkCliApp = InkCliApp;
39
+ const jsx_runtime_1 = require("react/jsx-runtime");
40
+ const react_1 = __importStar(require("react"));
41
+ const ink_1 = require("ink");
42
+ const streaming_markdown_1 = require("./streaming-markdown");
43
+ const spinner_1 = require("./spinner");
44
+ const tool_render_1 = require("./tool-render");
45
+ const phrases_1 = require("../../mind/phrases");
46
+ const format_1 = require("../../mind/format");
47
+ const runtime_1 = require("../../nerves/runtime");
48
+ /**
49
+ * Ink CLI Adapter — bridges RunCliSessionOptions with Ink/React rendering.
50
+ *
51
+ * Architecture:
52
+ * - CliStore: mutable external state, updated by callbacks
53
+ * - InkCliApp: reads from store via subscription, renders Ink components
54
+ * - createInkCallbacks: returns ChannelCallbacks that update the store
55
+ *
56
+ * The business logic (runAgent, pipeline, commands) stays unchanged.
57
+ * Only the rendering layer is replaced: readline -> Ink.
58
+ */
59
+ // Ouroboros brand palette
60
+ const OURO_GREEN = "#2ecc40";
61
+ const OURO_TEAL = "#4ec9b0";
62
+ /**
63
+ * Mutable external store. Callbacks mutate state; React subscribes via
64
+ * useSyncExternalStore pattern (simplified: we use a version counter +
65
+ * forceUpdate in the component).
66
+ */
67
+ class CliStore {
68
+ state = {
69
+ streamingText: "",
70
+ loading: false,
71
+ spinnerPhrase: "",
72
+ activeTool: null,
73
+ toolResults: [],
74
+ errorMessage: null,
75
+ kickMessage: null,
76
+ inputSuppressed: false,
77
+ bannerLines: [],
78
+ };
79
+ listeners = new Set();
80
+ spinnerStartTime = 0;
81
+ hadToolRun = false;
82
+ getState() {
83
+ return this.state;
84
+ }
85
+ subscribe(listener) {
86
+ this.listeners.add(listener);
87
+ return () => { this.listeners.delete(listener); };
88
+ }
89
+ notify() {
90
+ for (const fn of this.listeners)
91
+ fn();
92
+ }
93
+ // ─── Mutation methods called by callbacks ───
94
+ modelStart() {
95
+ const phrases = (0, phrases_1.getPhrases)();
96
+ const pool = this.hadToolRun ? phrases.followup : phrases.thinking;
97
+ const first = (0, phrases_1.pickPhrase)(pool);
98
+ this.state = {
99
+ ...this.state,
100
+ loading: true,
101
+ spinnerPhrase: first,
102
+ streamingText: "",
103
+ activeTool: null,
104
+ toolResults: [],
105
+ errorMessage: null,
106
+ kickMessage: null,
107
+ };
108
+ this.spinnerStartTime = Date.now();
109
+ this.notify();
110
+ }
111
+ clearText() {
112
+ this.state = { ...this.state, streamingText: "" };
113
+ this.notify();
114
+ }
115
+ appendText(text) {
116
+ this.state = {
117
+ ...this.state,
118
+ loading: false,
119
+ streamingText: this.state.streamingText + text,
120
+ };
121
+ this.notify();
122
+ }
123
+ toolStart(name, args) {
124
+ this.hadToolRun = true;
125
+ this.state = {
126
+ ...this.state,
127
+ loading: true,
128
+ activeTool: { name, args },
129
+ spinnerPhrase: (0, phrases_1.pickPhrase)((0, phrases_1.getPhrases)().tool),
130
+ };
131
+ this.spinnerStartTime = Date.now();
132
+ this.notify();
133
+ }
134
+ toolEnd(name, argSummary, success) {
135
+ this.state = {
136
+ ...this.state,
137
+ loading: false,
138
+ activeTool: null,
139
+ toolResults: [...this.state.toolResults, { name, argSummary, success }],
140
+ };
141
+ this.notify();
142
+ }
143
+ setError(message) {
144
+ this.state = { ...this.state, loading: false, errorMessage: message };
145
+ this.notify();
146
+ }
147
+ setKick() {
148
+ this.state = { ...this.state, loading: false, kickMessage: (0, format_1.formatKick)() };
149
+ this.notify();
150
+ }
151
+ suppressInput() {
152
+ this.state = { ...this.state, inputSuppressed: true };
153
+ this.notify();
154
+ }
155
+ restoreInput() {
156
+ this.state = { ...this.state, inputSuppressed: false };
157
+ this.notify();
158
+ }
159
+ /** Flush streaming state at end of turn */
160
+ endTurn() {
161
+ this.state = {
162
+ ...this.state,
163
+ loading: false,
164
+ activeTool: null,
165
+ errorMessage: null,
166
+ kickMessage: null,
167
+ };
168
+ this.notify();
169
+ }
170
+ setBanner(lines) {
171
+ this.state = { ...this.state, bannerLines: lines };
172
+ this.notify();
173
+ }
174
+ getElapsedSeconds() {
175
+ if (!this.state.loading)
176
+ return 0;
177
+ return Math.floor((Date.now() - this.spinnerStartTime) / 1000);
178
+ }
179
+ }
180
+ exports.CliStore = CliStore;
181
+ // ─── Callbacks factory ─────────────────────────────────────────────
182
+ function createInkCallbacks(store) {
183
+ (0, runtime_1.emitNervesEvent)({
184
+ component: "senses",
185
+ event: "senses.cli_callbacks_created",
186
+ message: "ink cli callbacks created",
187
+ meta: {},
188
+ });
189
+ return {
190
+ onModelStart: () => { store.modelStart(); },
191
+ onModelStreamStart: () => { },
192
+ onClearText: () => { store.clearText(); },
193
+ onTextChunk: (text) => { store.appendText(text); },
194
+ onReasoningChunk: (_text) => { },
195
+ onToolStart: (name, args) => { store.toolStart(name, args); },
196
+ onToolEnd: (name, argSummary, success) => { store.toolEnd(name, argSummary, success); },
197
+ onError: (error, severity) => {
198
+ if (severity === "transient") {
199
+ store.setError(error.message);
200
+ }
201
+ else {
202
+ store.setError((0, format_1.formatError)(error));
203
+ }
204
+ },
205
+ onKick: () => { store.setKick(); },
206
+ flushMarkdown: () => { store.endTurn(); },
207
+ };
208
+ }
209
+ // ─── Input Area ────────────────────────────────────────────────────
210
+ function InputArea({ onSubmit, suppressed, }) {
211
+ const [input, setInput] = (0, react_1.useState)("");
212
+ const inputRef = (0, react_1.useRef)("");
213
+ const [history, setHistory] = (0, react_1.useState)([]);
214
+ const historyIdx = (0, react_1.useRef)(-1);
215
+ (0, ink_1.useInput)((inputChar, key) => {
216
+ if (suppressed)
217
+ return;
218
+ if (key.return) {
219
+ const text = inputRef.current;
220
+ if (text.trim()) {
221
+ onSubmit(text);
222
+ setHistory(prev => {
223
+ const next = [...prev, text];
224
+ return next;
225
+ });
226
+ historyIdx.current = -1;
227
+ }
228
+ inputRef.current = "";
229
+ setInput("");
230
+ return;
231
+ }
232
+ if (key.backspace || key.delete) {
233
+ inputRef.current = inputRef.current.slice(0, -1);
234
+ setInput(inputRef.current);
235
+ return;
236
+ }
237
+ // History navigation
238
+ if (key.upArrow) {
239
+ if (history.length > 0) {
240
+ const idx = historyIdx.current < 0 ? history.length - 1 : Math.max(0, historyIdx.current - 1);
241
+ historyIdx.current = idx;
242
+ inputRef.current = history[idx];
243
+ setInput(history[idx]);
244
+ }
245
+ return;
246
+ }
247
+ if (key.downArrow) {
248
+ if (historyIdx.current >= 0) {
249
+ const idx = historyIdx.current + 1;
250
+ if (idx >= history.length) {
251
+ historyIdx.current = -1;
252
+ inputRef.current = "";
253
+ setInput("");
254
+ }
255
+ else {
256
+ historyIdx.current = idx;
257
+ inputRef.current = history[idx];
258
+ setInput(history[idx]);
259
+ }
260
+ }
261
+ return;
262
+ }
263
+ if (!key.ctrl && !key.meta && inputChar) {
264
+ inputRef.current += inputChar;
265
+ setInput(inputRef.current);
266
+ }
267
+ });
268
+ if (suppressed) {
269
+ return (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" });
270
+ }
271
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO_TEAL, bold: true, children: ") " }), (0, jsx_runtime_1.jsx)(ink_1.Text, { children: input })] }));
272
+ }
273
+ function InkCliApp({ store, onSubmit, onExit }) {
274
+ const { exit } = (0, ink_1.useApp)();
275
+ const [, forceUpdate] = (0, react_1.useState)(0);
276
+ const [elapsed, setElapsed] = (0, react_1.useState)(0);
277
+ // Subscribe to store changes
278
+ (0, react_1.useEffect)(() => {
279
+ return store.subscribe(() => {
280
+ forceUpdate(n => n + 1);
281
+ });
282
+ }, [store]);
283
+ // Elapsed timer for spinner
284
+ (0, react_1.useEffect)(() => {
285
+ const iv = setInterval(() => {
286
+ setElapsed(store.getElapsedSeconds());
287
+ }, 1000);
288
+ return () => clearInterval(iv);
289
+ }, [store]);
290
+ // Ctrl-C handling
291
+ (0, ink_1.useInput)((_input, key) => {
292
+ if (key.ctrl && _input === "c") {
293
+ if (onExit) {
294
+ onExit();
295
+ }
296
+ else {
297
+ exit();
298
+ }
299
+ }
300
+ });
301
+ const state = store.getState();
302
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [state.bannerLines.length > 0 ? ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", children: state.bannerLines.map((line, i) => ((0, jsx_runtime_1.jsx)(ink_1.Text, { children: line }, i))) })) : null, state.streamingText ? ((0, jsx_runtime_1.jsx)(streaming_markdown_1.StreamingMarkdown, { text: state.streamingText })) : null, state.toolResults.map((tr, i) => {
303
+ const color = tr.success ? OURO_GREEN : "#e74c3c";
304
+ const icon = tr.success ? "\u2713" : "\u2717";
305
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsxs)(ink_1.Text, { color: color, children: [icon, " "] }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO_TEAL, bold: true, children: tr.name }), (0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [" ", tr.argSummary] })] }, i));
306
+ }), state.activeTool ? ((0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(tool_render_1.ToolBadge, { name: state.activeTool.name }), (0, jsx_runtime_1.jsx)(tool_render_1.ToolParams, { name: state.activeTool.name, args: state.activeTool.args })] })) : null, state.loading ? ((0, jsx_runtime_1.jsx)(spinner_1.EnhancedSpinner, { elapsedSeconds: elapsed, phrase: state.spinnerPhrase })) : null, state.errorMessage ? ((0, jsx_runtime_1.jsx)(ink_1.Text, { color: "#e74c3c", children: state.errorMessage })) : null, state.kickMessage ? ((0, jsx_runtime_1.jsx)(ink_1.Text, { color: "#f39c12", children: state.kickMessage })) : null, (0, jsx_runtime_1.jsx)(InputArea, { onSubmit: onSubmit, suppressed: state.inputSuppressed })] }));
307
+ }