@sns-myagent/cli 0.3.6 → 0.3.7

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
@@ -60,7 +60,7 @@
60
60
  | Feature | Source |
61
61
  |---------|--------|
62
62
  | **30 built-in tools** | `src/tools/builtin-names.ts` |
63
- | **61 slash commands** | `src/slash-commands/builtin-registry.ts` |
63
+ | **157 built-in slash commands** | `src/slash-commands/builtin-registry.ts` |
64
64
  | **Multi-provider LLM** | OpenAI, Anthropic, Ollama, custom endpoints via `@oh-my-pi/pi-ai` |
65
65
  | **7 memory backends** | mnemopi (default), hindsight, mnemosyne, mem0, lcm, local, off — `src/memory-backend/resolve.ts` |
66
66
  | **MCP integration** | 22 source files in `src/mcp/` |
@@ -597,6 +597,56 @@ MIT — see [LICENSE](LICENSE).
597
597
 
598
598
  ---
599
599
 
600
+ ## Implementation Status (v0.3.7)
601
+
602
+ What is actually wired and working in the source tree, verified 2026-06-30:
603
+
604
+ ### Tools (30 / 30) — `src/tools/builtin-names.ts`
605
+
606
+ All 30 tools are real implementations, not stubs.
607
+
608
+ ### Slash Commands (157) — `src/slash-commands/builtin-registry.ts`
609
+
610
+ 157 commands registered. All callable via `/<name>` in interactive mode.
611
+
612
+ ### Memory Backends (7 / 7) — `src/memory-backend/resolve.ts`
613
+
614
+ | Backend | Status | Requires config |
615
+ |---------|--------|-----------------|
616
+ | `mnemopi` (default) | ✅ Works out of box | None — local SQLite + vector + graph |
617
+ | `local` | ✅ Works out of box | None — rollout summary pipeline |
618
+ | `off` | ✅ Works out of box | None — no-op |
619
+ | `mnemosyne` | ✅ Wired | `mnemosyne` Python daemon running locally |
620
+ | `mem0` | ✅ Wired | `MEM0_API_KEY` (or self-hosted endpoint) |
621
+ | `lcm` | ✅ Wired | `LCM_HOST` (default `127.0.0.1:8500`) |
622
+ | `hindsight` | ✅ Wired | `HINDSIGHT_API_KEY` or remote endpoint |
623
+
624
+ Set via `memory.backend` in `~/.sns-myagent/config.yaml`. Default is `mnemopi`.
625
+
626
+ ### Telegram Adapter — `src/adapters/telegram/`
627
+
628
+ 5 files (bot, handler, format, index, bridge). 10 slash commands wired: `/start /help /chat /reset /status /memory /cron /model /code /review`. File upload/download supported. Auto-boot when `SNS_TELEGRAM_BOT_TOKEN` is set.
629
+
630
+ ### Multi-Agent Orchestration — `src/agents/`
631
+
632
+ 7 files. `agents.yaml` config, 3 ensemble strategies (consensus / critic / best-of-N), resilience (retry + circuit-breaker + timeout), 21 tests pass. CLI `orchestrate <prompt>` is wired but executor is stubbed (returns clear "not wired yet" message).
633
+
634
+ ### Terminal UI — `src/tui/` + `src/ui/`
635
+
636
+ 8 files. `MY snsagent` splash, `●` prefix chat blocks, flat status bar, command palette, error display, memory toast. No gradient, no rounded boxes — flat list style. v0.3.6+ rebrand.
637
+
638
+ ### CI / CD
639
+
640
+ GitHub Actions runs 7-stage pipeline: typecheck, lint, build, test, diagnose, smoke, all-checks. 5 platform binaries (linux x64/arm64, macos x64/arm64, windows x64) auto-built on tag push. Docker multi-arch image auto-built and pushed to `ghcr.io/reihantt6/sns-myagent`.
641
+
642
+ ### Published
643
+
644
+ - npm: `npm install -g @sns-myagent/cli` → v0.3.7
645
+ - GitHub releases: 5 binaries per version
646
+ - Docker: `ghcr.io/reihantt6/sns-myagent:v0.3.7`
647
+
648
+ ---
649
+
600
650
  ## Credits
601
651
 
602
652
  Forked from [Pi Agent / oh-my-pi](https://github.com/can1357/oh-my-pi) by can1357. Stripped to a focused, single-user terminal agent with conversational configuration.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@sns-myagent/cli",
4
- "version": "0.3.6",
4
+ "version": "0.3.7",
5
5
  "description": "SNS-MyAgent \u2014 BYOK coding agent CLI. Multi-agent + Telegram + self-learning memory. ~120MB binary, zero forced cost.",
6
6
  "homepage": "https://github.com/Reihantt6/sns-myagent",
7
7
  "author": "Reihantt6",
@@ -1,22 +1,11 @@
1
1
  /**
2
- * Chat message block renderers single-accent bordered blocks.
3
- * One border style (rounded), one accent color (cyan). No role-specific
4
- * rainbow. Per-role distinction via bold label only.
2
+ * Chat blocks flat `●` prefix style.
3
+ * No rounded boxes, no per-role color. One accent (cyan) for the bullet.
4
+ * Role distinction via the bullet label only.
5
5
  */
6
6
  import chalk from "chalk";
7
7
  import { visibleWidth } from "@oh-my-pi/pi-tui";
8
8
 
9
- const BOX = {
10
- topLeft: "╭",
11
- topRight: "╮",
12
- bottomLeft: "╰",
13
- bottomRight: "╯",
14
- horizontal: "─",
15
- vertical: "│",
16
- } as const;
17
-
18
- const ACCENT = chalk.cyan;
19
-
20
9
  export type MessageRole = "user" | "assistant" | "tool" | "system" | "error";
21
10
 
22
11
  const ROLE_LABEL: Record<MessageRole, string> = {
@@ -27,6 +16,8 @@ const ROLE_LABEL: Record<MessageRole, string> = {
27
16
  error: "error",
28
17
  };
29
18
 
19
+ const BULLET = chalk.cyan("●");
20
+
30
21
  export interface ChatBlockOptions {
31
22
  role: MessageRole;
32
23
  label?: string;
@@ -64,65 +55,42 @@ function wrapAnsi(text: string, maxWidth: number): string[] {
64
55
 
65
56
  export function renderChatBlock(opts: ChatBlockOptions): string {
66
57
  const cols = process.stdout.columns ?? 80;
67
- const width = Math.min(opts.width ?? cols, cols) - 2;
68
- const pad = 1;
69
- const innerWidth = width - 2 - pad * 2;
58
+ const width = opts.width ?? Math.min(cols - 4, 80);
59
+ const innerWidth = width - 6; // " ● label "
70
60
 
71
- const lines: string[] = [];
72
-
73
- // Top bar: accent line + label
74
61
  const label = opts.label ?? ROLE_LABEL[opts.role];
75
- const labelText = ` ${label} `;
76
- const topFill = BOX.horizontal.repeat(Math.max(0, innerWidth - visibleWidth(labelText) + pad * 2));
77
- lines.push(ACCENT(BOX.topLeft + BOX.horizontal.repeat(2)) + chalk.bold(labelText) + ACCENT(topFill + BOX.topRight));
62
+ const headerRight = opts.meta ? chalk.dim(opts.meta) : "";
78
63
 
79
- // Content
64
+ // Header: `● label meta`
65
+ const headerFillLen = Math.max(0, innerWidth - visibleWidth(label) - visibleWidth(headerRight));
66
+ const headerFill = " ".repeat(headerFillLen);
67
+ const header = ` ${BULLET} ${chalk.bold(label)}${headerFill}${headerRight ? " " + headerRight : ""}`;
68
+
69
+ // Content: indent continuation lines with `│` for visual grouping
80
70
  const contentLines = wrapAnsi(opts.content, innerWidth);
81
- const padStr = " ".repeat(pad);
82
- for (const line of contentLines) {
83
- const vis = visibleWidth(line);
84
- const fill = " ".repeat(Math.max(0, innerWidth - vis));
85
- lines.push(ACCENT(BOX.vertical) + padStr + line + fill + padStr + ACCENT(BOX.vertical));
86
- }
71
+ const indented = contentLines.map((l) => ` ${chalk.dim("│")} ${l}`);
87
72
 
88
- // Meta
89
- if (opts.meta) {
90
- const metaText = chalk.dim(opts.meta);
91
- const metaFill = " ".repeat(Math.max(0, innerWidth - visibleWidth(opts.meta)));
92
- lines.push(ACCENT(BOX.vertical) + padStr + metaText + metaFill + padStr + ACCENT(BOX.vertical));
93
- }
73
+ const parts: string[] = [header, ...indented];
94
74
 
95
- // Streaming
96
75
  if (opts.streaming) {
97
- const frames = ["", "", "", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
98
- const frameIdx = Date.now() % (frames.length * 80);
99
- const frame = frames[Math.floor(frameIdx / 80)];
100
- const spinnerText = `${ACCENT(frame)} ${chalk.dim("thinking...")}`;
101
- const spinFill = " ".repeat(Math.max(0, innerWidth - visibleWidth(spinnerText)));
102
- lines.push(ACCENT(BOX.vertical) + padStr + spinnerText + spinFill + padStr + ACCENT(BOX.vertical));
76
+ parts.push(` ${chalk.dim("")} ${chalk.cyan("")} ${chalk.dim("thinking")}`);
103
77
  }
104
78
 
105
- // Bottom bar
106
- lines.push(ACCENT(BOX.bottomLeft + BOX.horizontal.repeat(width - 2) + BOX.bottomRight));
107
-
108
- return lines.join("\n");
79
+ return parts.join("\n");
109
80
  }
110
81
 
111
82
  /** Compact inline message — no box. */
112
83
  export function renderInline(role: MessageRole, text: string): string {
113
- const label = chalk.bold(`[${ROLE_LABEL[role]}]`);
114
- return `${label} ${text}`;
84
+ return `${BULLET} ${chalk.bold(ROLE_LABEL[role])} ${text}`;
115
85
  }
116
86
 
117
- /** Separator line, optional label. */
87
+ /** Separator line single dim rule. */
118
88
  export function renderDivider(label?: string): string {
119
89
  const cols = process.stdout.columns ?? 80;
120
- if (!label) {
121
- return chalk.dim("─".repeat(cols - 2));
122
- }
90
+ if (!label) return chalk.dim("─".repeat(cols - 2));
123
91
  const labelText = ` ${label} `;
124
92
  const fill = "─".repeat(Math.max(0, cols - 2 - visibleWidth(labelText)));
125
- return chalk.dim(fill.slice(0, Math.floor(fill.length / 2))) + chalk.dim(labelText) + chalk.dim(fill.slice(Math.floor(fill.length / 2)));
93
+ return chalk.dim(fill) + chalk.dim(labelText) + chalk.dim(fill);
126
94
  }
127
95
 
128
96
  /** Tool-call status line. */
@@ -131,18 +99,13 @@ export function renderToolBlock(
131
99
  status: "running" | "done" | "error",
132
100
  detail?: string,
133
101
  ): string {
134
- const icon = status === "running" ? chalk.cyan("") : status === "done" ? chalk.green("") : chalk.red("");
102
+ const icon = status === "running" ? chalk.cyan("") : status === "done" ? chalk.green("") : chalk.red("");
135
103
  const label = ` ${icon} tool:${toolName} `;
136
104
  const detailText = detail ? chalk.dim(` ${detail}`) : "";
137
105
  return label + detailText;
138
106
  }
139
107
 
140
- /** Session header. */
108
+ /** Session header — single line. */
141
109
  export function renderSessionHeader(model: string, version: string): string {
142
- const cols = process.stdout.columns ?? 80;
143
- const left = ACCENT.bold(" snsagent");
144
- const ver = chalk.dim(` v${version}`);
145
- const modelStr = chalk.dim(" · ") + chalk.cyan(model);
146
- const sep = chalk.dim("─".repeat(Math.max(0, cols - visibleWidth(left + ver + modelStr) - 2)));
147
- return `\n${left}${ver}${modelStr}\n${sep}\n`;
110
+ return ` ${chalk.cyan.bold("MY")} ${chalk.bold("snsagent")} ${chalk.dim(`v${version}`)} ${chalk.dim("·")} ${chalk.cyan(model)}\n`;
148
111
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Command palette — fuzzy command overlay.
3
- * Minimal: cyan highlight, dim for secondary. No gradient.
2
+ * Command palette — flat list, no box.
3
+ * Cyan highlight on selected row, dim otherwise.
4
4
  */
5
5
 
6
6
  import chalk from "chalk";
@@ -36,7 +36,7 @@ export const CHAT_COMMANDS: PaletteCommand[] = [
36
36
  ];
37
37
 
38
38
  export function renderCommandPalette(opts: CommandPaletteOptions): string {
39
- const { commands, query = "", highlighted = 0, width = 60 } = opts;
39
+ const { commands, query = "", highlighted = 0 } = opts;
40
40
  const filtered = commands.filter(
41
41
  (c) => c.enabled !== false &&
42
42
  (c.name.toLowerCase().includes(query.toLowerCase()) ||
@@ -44,33 +44,28 @@ export function renderCommandPalette(opts: CommandPaletteOptions): string {
44
44
  c.category.toLowerCase().includes(query.toLowerCase()))
45
45
  );
46
46
 
47
- const cols = process.stdout.columns ?? 80;
48
- const width2 = Math.min(opts.width ?? cols, cols) - 4;
49
-
50
47
  const lines: string[] = [];
51
48
 
52
- // Header
53
- const search = chalk.cyan("? ") + query + chalk.dim(" (↑↓ navigate, Enter select, Esc cancel)");
54
- lines.push(chalk.dim("─".repeat(60)));
55
- lines.push(` ${chalk.cyan("?")} ${query}${chalk.dim(" (↑↓ Enter Esc)")}`);
49
+ // Search line
50
+ lines.push(` ${chalk.cyan("?")} ${query}${chalk.dim(" (↑↓ navigate · Enter select · Esc cancel)")}`);
56
51
  lines.push("");
57
52
 
58
53
  if (filtered.length === 0) {
59
- lines.push(chalk.dim(" No commands match"));
54
+ lines.push(` ${chalk.dim("no commands match")}`);
60
55
  } else {
61
56
  filtered.forEach((cmd, idx) => {
62
57
  const isActive = idx === highlighted;
63
- const prefix = isActive ? chalk.cyan("") : " ";
58
+ const prefix = isActive ? chalk.cyan("") : " ";
64
59
  const name = isActive ? chalk.cyan.bold(cmd.name) : chalk.cyan(cmd.name);
65
60
  const desc = chalk.dim(cmd.description);
66
- const cat = chalk.dim(` [${cmd.category}]`);
67
- const shortcut = cmd.shortcut ? chalk.dim(` ${cmd.shortcut}`) : "";
68
- lines.push(`${prefix}${name}${cat}${shortcut}`);
61
+ const cat = chalk.dim(` [${cmd.category}]`);
62
+ const shortcut = cmd.shortcut ? chalk.dim(` ${cmd.shortcut}`) : "";
63
+ lines.push(` ${prefix} ${name}${cat}${shortcut}`);
64
+ if (isActive) {
65
+ lines.push(` ${desc}`);
66
+ }
69
67
  });
70
68
  }
71
69
 
72
- lines.push("");
73
- lines.push(chalk.dim("─".repeat(60)));
74
-
75
70
  return lines.join("\n");
76
- }
71
+ }
package/src/tui/splash.ts CHANGED
@@ -1,14 +1,13 @@
1
1
  /**
2
- * SNS-MyAgent splash — minimal banner + info block.
3
- * Single accent (cyan) for brand, default terminal text everywhere else.
2
+ * SNS-MyAgent splash — flat list, no boxes.
3
+ * Single line brand + `●` prefixed info rows. No rounded borders, no
4
+ * gradient, no separator boxes. Reads like a status line.
4
5
  */
5
6
  import chalk from "chalk";
6
7
  import { readFileSync } from "node:fs";
7
8
  import { dirname, resolve } from "node:path";
8
9
  import { fileURLToPath } from "node:url";
9
10
 
10
- const SUBTITLE = "snsagent — coding agent CLI";
11
-
12
11
  function readVersion(): string {
13
12
  try {
14
13
  const here = dirname(fileURLToPath(import.meta.url));
@@ -49,38 +48,39 @@ export interface SplashInfo {
49
48
  nodeVersion?: string;
50
49
  }
51
50
 
52
- export function renderSplash(info: SplashInfo = {}): string {
53
- const cols = process.stdout.columns ?? 80;
54
- const width = Math.min(cols - 4, 72);
51
+ /**
52
+ * One-line prefix used throughout the TUI.
53
+ * `●` = present, default text. No rounded boxes.
54
+ */
55
+ const BULLET = chalk.cyan("●");
55
56
 
56
- // Single brand mark, no rainbow
57
- const brandLine = `\n ${chalk.cyan.bold("MY")} ${chalk.bold("snsagent")} ${chalk.dim(`v${readVersion()}`)}\n`;
58
- const subLine = ` ${chalk.dim(SUBTITLE)}\n`;
57
+ export function renderSplash(info: SplashInfo = {}): string {
58
+ const ver = readVersion();
59
+ const lines: string[] = [];
59
60
 
60
- // Separator
61
- const sep = chalk.dim("".repeat(Math.min(width - 4, 60)));
61
+ // Brand line: MY · snsagent · v0.3.6
62
+ lines.push(` ${chalk.cyan.bold("MY")} ${chalk.bold("snsagent")} ${chalk.dim(`v${ver}`)}`);
63
+ lines.push(` ${chalk.dim("coding agent CLI")}`);
64
+ lines.push("");
62
65
 
63
- // Info rows: key dim, value default
64
- const rows: string[] = [sep];
65
- const kv = (k: string, v: string) =>
66
- ` ${chalk.dim(k.padEnd(12))}${v}`;
67
- if (info.model) rows.push(kv("Model", `${info.provider ?? "unknown"}/${info.model}`));
68
- if (info.cwd) rows.push(kv("Working Dir", info.cwd));
69
- if (info.platform) rows.push(kv("Platform", info.platform));
70
- rows.push(kv("Version", readVersion()));
71
- rows.push(sep);
66
+ // Status lines flat, one per line, ● prefix
67
+ const row = (label: string, value: string) => ` ${BULLET} ${chalk.dim(label.padEnd(13))}${value}`;
68
+ if (info.model) lines.push(row("model", `${info.provider ?? "unknown"}/${info.model}`));
69
+ if (info.cwd) lines.push(row("dir", info.cwd));
70
+ if (info.platform) lines.push(row("platform", info.platform));
71
+ lines.push(row("version", ver));
72
72
 
73
73
  // Hints
74
- rows.push(` ${chalk.dim("Type your message to start chatting.")}`);
75
- rows.push(` ${chalk.dim("/exit or Ctrl+C to quit.")}`);
74
+ lines.push("");
75
+ lines.push(` ${chalk.dim("type to chat · /exit to quit")}`);
76
76
 
77
- return brandLine + subLine + "\n" + rows.join("\n") + "\n";
77
+ return lines.join("\n") + "\n";
78
78
  }
79
79
 
80
80
  export function renderInlineHeader(info: SplashInfo = {}): string {
81
+ const ver = readVersion();
81
82
  const model = info.model
82
83
  ? chalk.cyan(`${info.provider ?? "?"}/${info.model}`)
83
84
  : chalk.dim("no model");
84
- const ver = chalk.dim(`v${readVersion()}`);
85
- return `\n ${chalk.cyan.bold("MY")} ${chalk.bold("snsagent")} ${ver} ${chalk.dim("│")} ${model}\n`;
85
+ return ` ${chalk.cyan.bold("MY")} ${chalk.bold("snsagent")} ${chalk.dim(`v${ver}`)} ${chalk.dim("·")} ${model}\n`;
86
86
  }
@@ -1,7 +1,6 @@
1
1
  /**
2
- * Error display — minimal, single accent, no gradient.
3
- * Severity controls the icon color (red/yellow/cyan) but everything else
4
- * stays dim + default text.
2
+ * Error display — flat `●` prefix, severity icon.
3
+ * No box, no gradient, no nested borders. Just one line per item.
5
4
  */
6
5
 
7
6
  import chalk from "chalk";
@@ -21,67 +20,61 @@ export interface ErrorDisplayOptions {
21
20
  function getSeverityMeta(severity: ErrorSeverity) {
22
21
  switch (severity) {
23
22
  case "error":
24
- return { icon: "", label: "ERROR", color: chalk.red.bold };
23
+ return { icon: chalk.red(""), label: "ERROR" };
25
24
  case "warning":
26
- return { icon: "", label: "WARNING", color: chalk.yellow.bold };
25
+ return { icon: chalk.yellow(""), label: "WARNING" };
27
26
  case "info":
28
- return { icon: "", label: "INFO", color: chalk.cyan.bold };
27
+ return { icon: chalk.cyan(""), label: "INFO" };
29
28
  }
30
29
  }
31
30
 
32
31
  export function renderErrorDisplay(opts: ErrorDisplayOptions): string {
33
32
  const severity = opts.severity ?? "error";
34
33
  const meta = getSeverityMeta(severity);
35
- const cols = process.stdout.columns ?? 80;
36
- const width = Math.min(cols - 2, 72);
37
-
38
- const border = "─".repeat(width - 2);
34
+ const codeStr = opts.code ? chalk.dim(` [${opts.code}]`) : "";
39
35
 
40
36
  const lines: string[] = [];
41
- lines.push(chalk.dim(border));
42
37
 
43
38
  // Header
44
- const codeStr = opts.code ? chalk.dim(` [${opts.code}]`) : "";
45
- lines.push(`${meta.color(meta.icon)} ${meta.color(meta.label)}${codeStr}`);
46
- lines.push("");
47
- lines.push(chalk.bold(opts.title));
48
- lines.push("");
39
+ lines.push(` ${meta.icon} ${chalk.bold(meta.label)}${codeStr}`);
40
+
41
+ // Title
42
+ lines.push(` ${chalk.bold(opts.title)}`);
49
43
 
50
44
  // Message
51
45
  for (const line of opts.message.split("\n")) {
52
- lines.push(line);
46
+ lines.push(` ${line}`);
53
47
  }
54
48
 
55
49
  // Context
56
50
  if (opts.context && opts.context.length > 0) {
57
51
  lines.push("");
58
- lines.push(chalk.dim("Context:"));
52
+ lines.push(` ${chalk.dim("context:")}`);
59
53
  for (const ctx of opts.context.slice(0, 5)) {
60
- lines.push(chalk.dim(` ${ctx}`));
54
+ lines.push(` ${chalk.dim(ctx)}`);
61
55
  }
62
56
  if (opts.context.length > 5) {
63
- lines.push(chalk.dim(` ... +${opts.context.length - 5} more`));
57
+ lines.push(` ${chalk.dim(`... +${opts.context.length - 5} more`)}`);
64
58
  }
65
59
  }
66
60
 
67
61
  // Suggestion
68
62
  if (opts.suggestion) {
69
63
  lines.push("");
70
- lines.push(`${chalk.cyan("→")} ${chalk.bold("Suggestion:")}`);
71
- lines.push(` ${opts.suggestion}`);
64
+ lines.push(` ${chalk.cyan("→")} ${chalk.bold("suggestion:")}`);
65
+ lines.push(` ${opts.suggestion}`);
72
66
  }
73
67
 
74
68
  // Stack preview
75
69
  if (opts.stack) {
76
70
  lines.push("");
77
71
  const stackLines = opts.stack.split("\n").slice(0, 3);
78
- lines.push(chalk.dim("Stack (first 3):"));
72
+ lines.push(` ${chalk.dim("stack (first 3):")}`);
79
73
  for (const sl of stackLines) {
80
- lines.push(chalk.dim(` ${sl.trim()}`));
74
+ lines.push(` ${chalk.dim(sl.trim())}`);
81
75
  }
82
76
  }
83
77
 
84
- lines.push(chalk.dim(border));
85
78
  return lines.join("\n");
86
79
  }
87
80
 
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Memory toast — brief non-intrusive recall/save notifications.
3
- * Single accent (cyan), no gradient, no per-type color explosion.
2
+ * Memory toast — flat `●` prefix, single accent.
4
3
  */
5
4
 
6
5
  import chalk from "chalk";
@@ -22,19 +21,10 @@ const TOAST_LABEL: Record<ToastType, string> = {
22
21
  info: "info",
23
22
  };
24
23
 
25
- const TOAST_ICON: Record<ToastType, string> = {
26
- recall: "·",
27
- save: "+",
28
- forget: "-",
29
- info: "i",
30
- };
31
-
32
24
  export function renderMemoryToast(opts: MemoryToastOptions): string {
33
- const icon = TOAST_ICON[opts.type];
34
- const label = chalk.cyan.bold(` ${TOAST_LABEL[opts.type]} `);
25
+ const label = chalk.cyan(`● ${TOAST_LABEL[opts.type]}`);
35
26
  const msg = opts.message;
36
-
37
- const parts = [chalk.dim(icon), label, msg];
27
+ const parts = [label, msg];
38
28
 
39
29
  if (opts.memoryId) {
40
30
  parts.push(chalk.dim(`#${opts.memoryId.slice(0, 8)}`));
@@ -42,33 +32,30 @@ export function renderMemoryToast(opts: MemoryToastOptions): string {
42
32
 
43
33
  if (opts.relevance !== undefined) {
44
34
  const score = Math.round(opts.relevance * 100);
45
- const scoreColor = score > 80 ? chalk.cyan : chalk.dim;
46
- parts.push(scoreColor(`(${score}%)`));
35
+ parts.push(chalk.cyan(`(${score}%)`));
47
36
  }
48
37
 
49
- return ` ${parts.join(" ")}`;
38
+ return ` ${parts.join(" ")}`;
50
39
  }
51
40
 
52
41
  export function renderMemoryRecall(query: string, snippet: string, relevance?: number): string {
53
42
  const queryStr = chalk.cyan(`"${query}"`);
54
43
  const scoreStr = relevance !== undefined
55
- ? chalk.dim(` ${Math.round(relevance * 100)}% match`)
44
+ ? chalk.dim(` ${Math.round(relevance * 100)}% match`)
56
45
  : "";
57
46
 
58
47
  const lines = [
59
- `${chalk.dim("·")} ${chalk.cyan.bold(" memory ")} ${queryStr}${scoreStr}`,
60
- ` ${chalk.dim(snippet.slice(0, 120))}${snippet.length > 120 ? chalk.dim("...") : ""}`,
48
+ ` ${chalk.cyan(" memory")} ${queryStr}${scoreStr}`,
49
+ ` ${chalk.dim(snippet.slice(0, 120))}${snippet.length > 120 ? chalk.dim("...") : ""}`,
61
50
  ];
62
51
 
63
52
  return lines.join("\n");
64
53
  }
65
54
 
66
55
  export function renderMemorySave(content: string, scope?: string): string {
67
- const label = chalk.cyan.bold(" saved ");
68
- const scopeStr = scope ? chalk.dim(` [${scope}]`) : "";
56
+ const scopeStr = scope ? chalk.dim(` [${scope}]`) : "";
69
57
  const preview = content.length > 80 ? content.slice(0, 80) + "..." : content;
70
-
71
- return `${chalk.dim("+")} ${label}${scopeStr} ${chalk.dim(preview)}`;
58
+ return ` ${chalk.cyan("● saved")}${scopeStr} ${chalk.dim(preview)}`;
72
59
  }
73
60
 
74
61
  export function clearToastLine(): void {
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Status bar — bottom-line summary.
3
- * Minimal: dim key + accent value. No gradient, no per-segment color.
2
+ * Status bar — flat single line, dim key + cyan value.
4
3
  */
5
4
 
6
5
  import chalk from "chalk";
@@ -27,12 +26,11 @@ export function renderStatusBar(state: StatusBarState): void {
27
26
  ? `${(state.tokensUsed / 1000).toFixed(1)}k`
28
27
  : String(state.tokensUsed);
29
28
 
30
- // Single dim/accent pair, no per-segment color
31
29
  const k = (s: string) => chalk.dim(s);
32
30
  const v = (s: string) => chalk.cyan(s);
33
31
  const sep = chalk.dim(" · ");
34
32
 
35
- const bar = ` ${k("model")} ${v(state.model)}${sep}${k("tokens")} ${v(tokens)}${sep}${k("time")} ${v(elapsed)}${sep}${k("mem")} ${v(String(state.memoryHits))} `;
33
+ const bar = ` ${k("model")} ${v(state.model)}${sep}${k("tokens")} ${v(tokens)}${sep}${k("time")} ${v(elapsed)}${sep}${k("mem")} ${v(String(state.memoryHits))}`;
36
34
 
37
35
  process.stdout.write(`\x1b[2K\r${bar}`);
38
36
  }