@sns-myagent/cli 0.3.6 → 0.3.8
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 +51 -1
- package/package.json +3 -3
- package/src/cli/index.ts +66 -2
- package/src/config/defaults.ts +5 -3
- package/src/config/schema.ts +8 -0
- package/src/tui/chat-blocks.ts +25 -62
- package/src/tui/command-palette.ts +14 -19
- package/src/tui/splash.ts +26 -26
- package/src/ui/error-display.ts +18 -25
- package/src/ui/memory-toast.ts +10 -23
- package/src/ui/status-bar.ts +2 -4
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
|
-
| **
|
|
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.
|
|
4
|
+
"version": "0.3.8",
|
|
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",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"scripts": {
|
|
36
36
|
"fetch-binary": "node scripts/fetch-binary.mjs",
|
|
37
37
|
"postinstall": "node scripts/fetch-binary.mjs && node scripts/apply-pi-natives-patch.js",
|
|
38
|
-
"build": "bun build --compile --define process.env.PI_COMPILED='\"true\"' --define process.env.PKG_VERSION='\"0.3.
|
|
38
|
+
"build": "bun build --compile --define process.env.PI_COMPILED='\"true\"' --define process.env.PKG_VERSION='\"0.3.8\"' --no-compile-autoload-bunfig --no-compile-autoload-dotenv --no-compile-autoload-tsconfig --no-compile-autoload-package-json --keep-names --outfile bin/snsagent-linux-x64 ./src/cli/entry.ts && cp bin/snsagent-linux-x64 bin/snsagent",
|
|
39
39
|
"check": "biome check . && bun run check:types",
|
|
40
40
|
"check:types": "tsgo -p tsconfig.json --noEmit",
|
|
41
41
|
"lint": "biome lint .",
|
|
@@ -117,4 +117,4 @@
|
|
|
117
117
|
"CHANGELOG.md",
|
|
118
118
|
"dist/types"
|
|
119
119
|
]
|
|
120
|
-
}
|
|
120
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -16,6 +16,9 @@ import type { Config } from "../config/schema.js";
|
|
|
16
16
|
import { defaultConfigFn } from "../config/loader.js";
|
|
17
17
|
import { startTelegramAdapter, stopTelegramAdapter } from "../adapters/telegram/index.js";
|
|
18
18
|
import { createForwardToAgent, resetChatSession, getBridgeStats } from "../adapters/telegram/bridge.js";
|
|
19
|
+
import { createInterface } from "node:readline/promises";
|
|
20
|
+
import { stdin, stdout } from "node:process";
|
|
21
|
+
import chalk from "chalk";
|
|
19
22
|
|
|
20
23
|
// ---------- package version (single source of truth) ----------
|
|
21
24
|
|
|
@@ -61,7 +64,7 @@ function cmdVersion(): number {
|
|
|
61
64
|
return 0;
|
|
62
65
|
}
|
|
63
66
|
|
|
64
|
-
function cmdInit(): number {
|
|
67
|
+
async function cmdInit(): Promise<number> {
|
|
65
68
|
const path = configPath();
|
|
66
69
|
try {
|
|
67
70
|
const existing = loadConfig();
|
|
@@ -69,9 +72,70 @@ function cmdInit(): number {
|
|
|
69
72
|
process.stdout.write(`✓ config already exists at ${path}\n`);
|
|
70
73
|
return 0;
|
|
71
74
|
}
|
|
75
|
+
|
|
76
|
+
// Interactive memory backend selection
|
|
77
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
78
|
+
|
|
79
|
+
process.stdout.write("\n");
|
|
80
|
+
process.stdout.write(` ${chalk.cyan("●")} ${chalk.bold("Memory Backend Selection")}\n`);
|
|
81
|
+
process.stdout.write(` ${chalk.dim("Select memory backend (default: mnemopi):")}\n`);
|
|
82
|
+
process.stdout.write(` ${chalk.cyan("1")} mnemopi (default) — SQLite + vector + graph, offline, no config\n`);
|
|
83
|
+
process.stdout.write(` ${chalk.cyan("2")} local — rollout summary only\n`);
|
|
84
|
+
process.stdout.write(` ${chalk.cyan("3")} mnemosyne — advanced local (Python daemon)\n`);
|
|
85
|
+
process.stdout.write(` ${chalk.cyan("4")} mem0 — cloud API (needs MEM0_API_KEY)\n`);
|
|
86
|
+
process.stdout.write(` ${chalk.cyan("5")} lcm — local server (needs LCM_HOST)\n`);
|
|
87
|
+
process.stdout.write(` ${chalk.cyan("6")} hindsight — cloud (needs HINDSIGHT_API_KEY)\n`);
|
|
88
|
+
process.stdout.write(` ${chalk.cyan("7")} off — disabled\n`);
|
|
89
|
+
process.stdout.write(` [1] `);
|
|
90
|
+
|
|
91
|
+
const answer = await rl.question("");
|
|
92
|
+
|
|
93
|
+
let backend: "mnemopi" | "hindsight" | "mnemosyne" | "mem0" | "lcm" | "local" | "off" = "mnemopi";
|
|
94
|
+
switch (answer.trim()) {
|
|
95
|
+
case "1":
|
|
96
|
+
case "":
|
|
97
|
+
backend = "mnemopi";
|
|
98
|
+
break;
|
|
99
|
+
case "2":
|
|
100
|
+
backend = "local";
|
|
101
|
+
break;
|
|
102
|
+
case "3":
|
|
103
|
+
backend = "mnemosyne";
|
|
104
|
+
break;
|
|
105
|
+
case "4":
|
|
106
|
+
backend = "mem0";
|
|
107
|
+
break;
|
|
108
|
+
case "5":
|
|
109
|
+
backend = "lcm";
|
|
110
|
+
break;
|
|
111
|
+
case "6":
|
|
112
|
+
backend = "hindsight";
|
|
113
|
+
break;
|
|
114
|
+
case "7":
|
|
115
|
+
backend = "off";
|
|
116
|
+
break;
|
|
117
|
+
default:
|
|
118
|
+
process.stdout.write(` ${chalk.dim("unknown option, using default (mnemopi)")}\n`);
|
|
119
|
+
backend = "mnemopi";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Mnemosyne needs Python daemon
|
|
123
|
+
if (backend === "mnemosyne") {
|
|
124
|
+
process.stdout.write(`\n ${chalk.cyan("●")} mnemosyne requires Python daemon.\n`);
|
|
125
|
+
process.stdout.write(` ${chalk.dim("Install now? (pip install mnemosyne) [Y/n] ")}`);
|
|
126
|
+
const yn = await rl.question("");
|
|
127
|
+
if (!yn || yn.toLowerCase() === "y") {
|
|
128
|
+
process.stdout.write(` ${chalk.dim("installing mnemosyne... (placeholder)")}\n`);
|
|
129
|
+
// TODO: actual pip install
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
rl.close();
|
|
134
|
+
|
|
72
135
|
const cfg = defaultConfigFn();
|
|
136
|
+
cfg.memory = { ...cfg.memory, backend };
|
|
73
137
|
saveConfig(cfg);
|
|
74
|
-
process.stdout.write(`✓ created ${path}\n`);
|
|
138
|
+
process.stdout.write(`✓ created ${path} (memory backend: ${backend})\n`);
|
|
75
139
|
return 0;
|
|
76
140
|
} catch (err) {
|
|
77
141
|
process.stderr.write(`✗ init failed: ${(err as Error).message}\n`);
|
package/src/config/defaults.ts
CHANGED
|
@@ -26,6 +26,7 @@ export const defaultConfig: Config = {
|
|
|
26
26
|
path: "memory.jsonl",
|
|
27
27
|
maxEntries: 1000,
|
|
28
28
|
autoSummarize: true,
|
|
29
|
+
backend: "mnemopi",
|
|
29
30
|
},
|
|
30
31
|
};
|
|
31
32
|
|
|
@@ -44,9 +45,10 @@ export const DEFAULT_CONFIG_YAML =
|
|
|
44
45
|
` allowedChatIds: []\n` +
|
|
45
46
|
` pollIntervalMs: ${defaultConfig.telegram.pollIntervalMs}\n` +
|
|
46
47
|
`memory:\n` +
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
` path: ${defaultConfig.memory.path}\n` +
|
|
49
|
+
` maxEntries: ${defaultConfig.memory.maxEntries}\n` +
|
|
50
|
+
` autoSummarize: ${defaultConfig.memory.autoSummarize}\n` +
|
|
51
|
+
` backend: ${defaultConfig.memory.backend}\n`;
|
|
50
52
|
|
|
51
53
|
/** Default LLM provider id. */
|
|
52
54
|
export const DEFAULT_PROVIDER = defaultConfig.model.provider;
|
package/src/config/schema.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface MemoryConfig {
|
|
|
32
32
|
maxEntries: number;
|
|
33
33
|
/** Auto-summarize old entries when count exceeds threshold. */
|
|
34
34
|
autoSummarize: boolean;
|
|
35
|
+
/** Memory backend to use. */
|
|
36
|
+
backend: "mnemopi" | "hindsight" | "mnemosyne" | "mem0" | "lcm" | "local" | "off";
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export interface Config {
|
|
@@ -109,6 +111,11 @@ export function validateConfig(raw: unknown): Config {
|
|
|
109
111
|
if (typeof mem.autoSummarize !== "boolean") {
|
|
110
112
|
throw new Error("config.memory.autoSummarize must be boolean");
|
|
111
113
|
}
|
|
114
|
+
// Optional backend field with default
|
|
115
|
+
const backend = mem.backend;
|
|
116
|
+
if (backend !== undefined && !isNonEmptyString(backend)) {
|
|
117
|
+
throw new Error("config.memory.backend must be a string if present");
|
|
118
|
+
}
|
|
112
119
|
|
|
113
120
|
const extra = r.extra;
|
|
114
121
|
if (extra !== undefined && (typeof extra !== "object" || extra === null || Array.isArray(extra))) {
|
|
@@ -133,6 +140,7 @@ export function validateConfig(raw: unknown): Config {
|
|
|
133
140
|
path: mem.path as string,
|
|
134
141
|
maxEntries: mem.maxEntries as number,
|
|
135
142
|
autoSummarize: mem.autoSummarize as boolean,
|
|
143
|
+
backend: (mem.backend ?? "mnemopi") as "mnemopi" | "hindsight" | "mnemosyne" | "mem0" | "lcm" | "local" | "off",
|
|
136
144
|
},
|
|
137
145
|
extra: extra as Record<string, unknown> | undefined,
|
|
138
146
|
};
|
package/src/tui/chat-blocks.ts
CHANGED
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Chat
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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 =
|
|
68
|
-
const
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
return `${label} ${text}`;
|
|
84
|
+
return `${BULLET} ${chalk.bold(ROLE_LABEL[role])} ${text}`;
|
|
115
85
|
}
|
|
116
86
|
|
|
117
|
-
/** Separator line
|
|
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
|
|
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("
|
|
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
|
-
|
|
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 —
|
|
3
|
-
*
|
|
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
|
|
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
|
-
//
|
|
53
|
-
|
|
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("
|
|
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(`
|
|
67
|
-
const shortcut = cmd.shortcut ? chalk.dim(`
|
|
68
|
-
lines.push(
|
|
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 —
|
|
3
|
-
* Single
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
57
|
+
export function renderSplash(info: SplashInfo = {}): string {
|
|
58
|
+
const ver = readVersion();
|
|
59
|
+
const lines: string[] = [];
|
|
59
60
|
|
|
60
|
-
//
|
|
61
|
-
|
|
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
|
-
//
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (info.
|
|
68
|
-
|
|
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
|
-
|
|
75
|
-
|
|
74
|
+
lines.push("");
|
|
75
|
+
lines.push(` ${chalk.dim("type to chat · /exit to quit")}`);
|
|
76
76
|
|
|
77
|
-
return
|
|
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
|
-
|
|
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
|
}
|
package/src/ui/error-display.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Error display —
|
|
3
|
-
*
|
|
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: "
|
|
23
|
+
return { icon: chalk.red("●"), label: "ERROR" };
|
|
25
24
|
case "warning":
|
|
26
|
-
return { icon: "
|
|
25
|
+
return { icon: chalk.yellow("●"), label: "WARNING" };
|
|
27
26
|
case "info":
|
|
28
|
-
return { icon: "
|
|
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
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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("
|
|
52
|
+
lines.push(` ${chalk.dim("context:")}`);
|
|
59
53
|
for (const ctx of opts.context.slice(0, 5)) {
|
|
60
|
-
lines.push(chalk.dim(
|
|
54
|
+
lines.push(` ${chalk.dim(ctx)}`);
|
|
61
55
|
}
|
|
62
56
|
if (opts.context.length > 5) {
|
|
63
|
-
lines.push(chalk.dim(
|
|
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(
|
|
71
|
-
lines.push(`
|
|
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("
|
|
72
|
+
lines.push(` ${chalk.dim("stack (first 3):")}`);
|
|
79
73
|
for (const sl of stackLines) {
|
|
80
|
-
lines.push(chalk.dim(
|
|
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
|
|
package/src/ui/memory-toast.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Memory toast —
|
|
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
|
|
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
|
-
|
|
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(`
|
|
44
|
+
? chalk.dim(` ${Math.round(relevance * 100)}% match`)
|
|
56
45
|
: "";
|
|
57
46
|
|
|
58
47
|
const lines = [
|
|
59
|
-
|
|
60
|
-
`
|
|
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
|
|
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 {
|
package/src/ui/status-bar.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Status bar —
|
|
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
|
}
|