@sns-myagent/cli 0.2.0 → 0.3.0

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 (61) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/README.md +7 -8
  3. package/package.json +3 -3
  4. package/scripts/apply-pi-natives-patch.js +82 -56
  5. package/src/adapters/telegram/bot.ts +41 -1
  6. package/src/adapters/telegram/bridge.ts +186 -0
  7. package/src/adapters/telegram/handler.ts +138 -13
  8. package/src/adapters/telegram/index.ts +12 -2
  9. package/src/agents/__tests__/config.test.ts +79 -0
  10. package/src/agents/__tests__/resilience.test.ts +119 -0
  11. package/src/agents/__tests__/strategies.test.ts +83 -0
  12. package/src/agents/config.ts +367 -0
  13. package/src/agents/ensemble.ts +224 -0
  14. package/src/agents/resilience.ts +332 -0
  15. package/src/agents/strategies/best-of-n.ts +108 -0
  16. package/src/agents/strategies/consensus.ts +105 -0
  17. package/src/agents/strategies/critic.ts +131 -0
  18. package/src/agents/strategies/types.ts +68 -0
  19. package/src/async/__tests__/task-runner.test.ts +162 -0
  20. package/src/async/__tests__/task-store.test.ts +146 -0
  21. package/src/async/index.ts +28 -1
  22. package/src/async/notifier.ts +133 -0
  23. package/src/async/task-runner.ts +175 -0
  24. package/src/async/task-store.ts +170 -0
  25. package/src/async/types.ts +70 -0
  26. package/src/cli/entry.ts +3 -1
  27. package/src/cli/index.ts +69 -54
  28. package/src/config/index.ts +1 -1
  29. package/src/debug/index.ts +1 -1
  30. package/src/modes/components/welcome.ts +13 -6
  31. package/src/modes/controllers/event-controller.ts +1 -1
  32. package/src/modes/setup-wizard/scenes/splash.ts +1 -1
  33. package/src/session/agent-session.ts +1 -1
  34. package/src/slash-commands/builtin-registry.ts +63 -0
  35. package/src/slash-commands/helpers/task.ts +181 -0
  36. package/src/tbm/__tests__/tbm.test.ts +660 -0
  37. package/src/tbm/comm-modes.ts +165 -0
  38. package/src/tbm/config.ts +136 -0
  39. package/src/tbm/context-delta.ts +146 -0
  40. package/src/tbm/context-pyramid.ts +202 -0
  41. package/src/tbm/dashboard.ts +131 -0
  42. package/src/tbm/index.ts +247 -0
  43. package/src/tbm/lazy-skills.ts +182 -0
  44. package/src/tbm/response-cache.ts +220 -0
  45. package/src/tbm/tombstone.ts +189 -0
  46. package/src/tbm/tool-compress.ts +230 -0
  47. package/src/tools/ask.ts +1 -1
  48. package/src/tui/chat-blocks.ts +205 -0
  49. package/src/tui/chat-ui.ts +270 -0
  50. package/src/tui/code-cell.ts +90 -1
  51. package/src/tui/command-palette.ts +189 -0
  52. package/src/tui/index.ts +4 -0
  53. package/src/tui/splash.ts +130 -0
  54. package/src/ui/banner.ts +69 -29
  55. package/src/ui/colors.ts +24 -0
  56. package/src/ui/error-display.ts +130 -0
  57. package/src/ui/gradient.ts +104 -0
  58. package/src/ui/index.ts +15 -0
  59. package/src/ui/memory-toast.ts +102 -0
  60. package/src/ui/status-bar.ts +36 -30
  61. package/bin/snscoder.js +0 -98
@@ -0,0 +1,175 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // Async Task Runner — background execution engine
3
+ // ═══════════════════════════════════════════════════════════════════════════
4
+
5
+ import type { AsyncTask, TaskExecutionResult, TaskRunnerOptions, NotifyCallback } from "./types";
6
+ import { TaskStore, getTaskStore } from "./task-store";
7
+
8
+ export type TaskExecutor = (task: AsyncTask) => Promise<TaskExecutionResult>;
9
+
10
+ export class TaskRunner {
11
+ #store: TaskStore;
12
+ #running: Map<string, AbortController> = new Map();
13
+ #executors: Map<string, TaskExecutor> = new Map();
14
+ #notifyCallbacks: NotifyCallback[] = [];
15
+ #maxConcurrent: number;
16
+ #defaultTimeoutMs: number;
17
+ #pollTimer: ReturnType<typeof setInterval> | null = null;
18
+ #destroyed = false;
19
+
20
+ constructor(store: TaskStore, options: TaskRunnerOptions = {}) {
21
+ this.#store = store;
22
+ this.#maxConcurrent = options.maxConcurrent ?? 3;
23
+ this.#defaultTimeoutMs = options.defaultTimeoutMs ?? 5 * 60 * 1000; // 5 min default
24
+ }
25
+
26
+ /** Register an executor for a task type */
27
+ registerExecutor(taskType: string, executor: TaskExecutor): void {
28
+ this.#executors.set(taskType, executor);
29
+ }
30
+
31
+ /** Register notification callback */
32
+ onNotify(callback: NotifyCallback): void {
33
+ this.#notifyCallbacks.push(callback);
34
+ }
35
+
36
+ /** Start the polling loop */
37
+ start(pollIntervalMs = 5000): void {
38
+ if (this.#pollTimer) return;
39
+ this.#pollTimer = setInterval(() => this.#tick(), pollIntervalMs);
40
+ }
41
+
42
+ /** Stop polling */
43
+ stop(): void {
44
+ if (this.#pollTimer) {
45
+ clearInterval(this.#pollTimer);
46
+ this.#pollTimer = null;
47
+ }
48
+ }
49
+
50
+ /** Submit a task for background execution */
51
+ async submit(task: AsyncTask): Promise<void> {
52
+ if (this.#running.size >= this.#maxConcurrent) {
53
+ // Will be picked up by poll when a slot opens
54
+ return;
55
+ }
56
+ this.#executeTask(task);
57
+ }
58
+
59
+ /** Cancel a running task */
60
+ cancel(taskId: string): boolean {
61
+ const controller = this.#running.get(taskId);
62
+ if (controller) {
63
+ controller.abort();
64
+ this.#running.delete(taskId);
65
+ this.#store.updateStatus(taskId, "cancelled");
66
+ return true;
67
+ }
68
+ // Maybe it's still pending
69
+ const task = this.#store.getById(taskId);
70
+ if (task && task.status === "pending") {
71
+ this.#store.updateStatus(taskId, "cancelled");
72
+ return true;
73
+ }
74
+ return false;
75
+ }
76
+
77
+ /** Get running task count */
78
+ get runningCount(): number {
79
+ return this.#running.size;
80
+ }
81
+
82
+ /** Check if a task is currently running */
83
+ isRunning(taskId: string): boolean {
84
+ return this.#running.has(taskId);
85
+ }
86
+
87
+ /** Destroy the runner — cancel all running tasks */
88
+ destroy(): void {
89
+ this.#destroyed = true;
90
+ this.stop();
91
+ for (const [id, controller] of this.#running) {
92
+ controller.abort();
93
+ this.#store.updateStatus(id, "cancelled");
94
+ }
95
+ this.#running.clear();
96
+ }
97
+
98
+ // ─── Internal ──────────────────────────────────────────────────────────
99
+
100
+ async #tick(): Promise<void> {
101
+ if (this.#destroyed) return;
102
+ if (this.#running.size >= this.#maxConcurrent) return;
103
+
104
+ const pending = this.#store.listPending(this.#maxConcurrent - this.#running.size);
105
+ for (const task of pending) {
106
+ if (this.#running.size >= this.#maxConcurrent) break;
107
+ this.#executeTask(task);
108
+ }
109
+ }
110
+
111
+ async #executeTask(task: AsyncTask): Promise<void> {
112
+ if (this.#destroyed || this.#running.has(task.id)) return;
113
+
114
+ const executor = this.#executors.get(task.taskType);
115
+ if (!executor) {
116
+ this.#store.updateStatus(task.id, "failed", {
117
+ error: `No executor registered for task type: ${task.taskType}`,
118
+ });
119
+ this.#notify(task);
120
+ return;
121
+ }
122
+
123
+ const controller = new AbortController();
124
+ this.#running.set(task.id, controller);
125
+ this.#store.updateStatus(task.id, "running");
126
+
127
+ // Timeout
128
+ const timeout = setTimeout(() => {
129
+ controller.abort();
130
+ }, this.#defaultTimeoutMs);
131
+
132
+ try {
133
+ const result = await executor(task);
134
+ clearTimeout(timeout);
135
+
136
+ if (this.#destroyed) return;
137
+
138
+ if (result.success) {
139
+ this.#store.updateStatus(task.id, "completed", { result: result.result });
140
+ } else {
141
+ this.#store.updateStatus(task.id, "failed", { error: result.error ?? "Unknown error" });
142
+ }
143
+ } catch (err) {
144
+ clearTimeout(timeout);
145
+ if (this.#destroyed) return;
146
+ const errorMsg = err instanceof Error ? err.message : String(err);
147
+ this.#store.updateStatus(task.id, "failed", { error: errorMsg });
148
+ } finally {
149
+ this.#running.delete(task.id);
150
+ // Refresh task from store for notification
151
+ const updated = this.#store.getById(task.id);
152
+ if (updated) this.#notify(updated);
153
+ }
154
+ }
155
+
156
+ async #notify(task: AsyncTask): Promise<void> {
157
+ for (const cb of this.#notifyCallbacks) {
158
+ try {
159
+ await cb(task, "cli");
160
+ } catch {
161
+ // Don't let notification errors bubble
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ /** Singleton accessor */
168
+ let _instance: TaskRunner | null = null;
169
+
170
+ export function getTaskRunner(store?: TaskStore, options?: TaskRunnerOptions): TaskRunner {
171
+ if (!_instance) {
172
+ _instance = new TaskRunner(store ?? getTaskStore(), options);
173
+ }
174
+ return _instance;
175
+ }
@@ -0,0 +1,170 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // Async Task Store — SQLite persistence
3
+ // Pattern: mirrors src/cron/cron-store.ts
4
+ // ═══════════════════════════════════════════════════════════════════════════
5
+
6
+ import { Database } from "bun:sqlite";
7
+ import { mkdirSync } from "node:fs";
8
+ import { dirname } from "node:path";
9
+ import type { AsyncTask, AsyncTaskRow, TaskStatus, CreateTaskOptions } from "./types";
10
+
11
+ const SCHEMA_SQL = `
12
+ CREATE TABLE IF NOT EXISTS async_tasks (
13
+ id TEXT PRIMARY KEY,
14
+ status TEXT NOT NULL DEFAULT 'pending',
15
+ task_type TEXT NOT NULL DEFAULT 'prompt',
16
+ description TEXT NOT NULL,
17
+ result TEXT,
18
+ error TEXT,
19
+ created_at INTEGER NOT NULL,
20
+ started_at INTEGER,
21
+ completed_at INTEGER,
22
+ metadata TEXT
23
+ );
24
+ CREATE INDEX IF NOT EXISTS idx_async_tasks_status ON async_tasks(status);
25
+ CREATE INDEX IF NOT EXISTS idx_async_tasks_created ON async_tasks(created_at);
26
+ `;
27
+
28
+ function rowToTask(row: AsyncTaskRow): AsyncTask {
29
+ return {
30
+ id: row.id,
31
+ status: row.status as TaskStatus,
32
+ taskType: row.task_type as AsyncTask["taskType"],
33
+ description: row.description,
34
+ result: row.result,
35
+ error: row.error,
36
+ createdAt: row.created_at,
37
+ startedAt: row.started_at,
38
+ completedAt: row.completed_at,
39
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
40
+ };
41
+ }
42
+
43
+ export class TaskStore {
44
+ #db: Database;
45
+
46
+ constructor(dbPath: string) {
47
+ mkdirSync(dirname(dbPath), { recursive: true });
48
+ this.#db = new Database(dbPath);
49
+ this.#db.exec("PRAGMA journal_mode = WAL");
50
+ this.#db.exec("PRAGMA foreign_keys = ON");
51
+ this.#db.exec(SCHEMA_SQL);
52
+ }
53
+
54
+ create(opts: CreateTaskOptions): AsyncTask {
55
+ const id = crypto.randomUUID();
56
+ const now = Date.now();
57
+ const taskType = opts.taskType ?? "prompt";
58
+
59
+ this.#db.run(
60
+ `INSERT INTO async_tasks (id, status, task_type, description, created_at, metadata)
61
+ VALUES (?, 'pending', ?, ?, ?, ?)`,
62
+ [id, taskType, opts.description, now, opts.metadata ? JSON.stringify(opts.metadata) : null],
63
+ );
64
+
65
+ return this.getById(id)!;
66
+ }
67
+
68
+ getById(id: string): AsyncTask | null {
69
+ const row = this.#db.query<AsyncTaskRow, [string]>(
70
+ "SELECT * FROM async_tasks WHERE id = ?",
71
+ ).get(id);
72
+ return row ? rowToTask(row) : null;
73
+ }
74
+
75
+ list(status?: TaskStatus, limit = 50): AsyncTask[] {
76
+ const rows = status
77
+ ? this.#db.query<AsyncTaskRow, [string, number]>(
78
+ "SELECT * FROM async_tasks WHERE status = ? ORDER BY created_at DESC LIMIT ?",
79
+ ).all(status, limit)
80
+ : this.#db.query<AsyncTaskRow, [number]>(
81
+ "SELECT * FROM async_tasks ORDER BY created_at DESC LIMIT ?",
82
+ ).all(limit);
83
+ return rows.map(rowToTask);
84
+ }
85
+
86
+ listPending(limit = 20): AsyncTask[] {
87
+ return this.list("pending", limit);
88
+ }
89
+
90
+ listRunning(): AsyncTask[] {
91
+ return this.list("running");
92
+ }
93
+
94
+ updateStatus(id: string, status: TaskStatus, extra?: { result?: string; error?: string }): boolean {
95
+ const now = Date.now();
96
+ let sql: string;
97
+ let params: (string | number | null)[];
98
+
99
+ switch (status) {
100
+ case "running":
101
+ sql = "UPDATE async_tasks SET status = ?, started_at = ? WHERE id = ?";
102
+ params = [status, now, id];
103
+ break;
104
+ case "completed":
105
+ sql = "UPDATE async_tasks SET status = ?, completed_at = ?, result = ? WHERE id = ?";
106
+ params = [status, now, extra?.result ?? null, id];
107
+ break;
108
+ case "failed":
109
+ sql = "UPDATE async_tasks SET status = ?, completed_at = ?, error = ? WHERE id = ?";
110
+ params = [status, now, extra?.error ?? null, id];
111
+ break;
112
+ case "cancelled":
113
+ sql = "UPDATE async_tasks SET status = ?, completed_at = ? WHERE id = ?";
114
+ params = [status, now, id];
115
+ break;
116
+ default:
117
+ sql = "UPDATE async_tasks SET status = ? WHERE id = ?";
118
+ params = [status, id];
119
+ }
120
+
121
+ const stmt = this.#db.prepare(sql);
122
+ const result = stmt.run(...params);
123
+ return result.changes > 0;
124
+ }
125
+
126
+ delete(id: string): boolean {
127
+ const result = this.#db.run("DELETE FROM async_tasks WHERE id = ?", [id]);
128
+ return result.changes > 0;
129
+ }
130
+
131
+ /** Clean up completed/failed tasks older than maxAgeMs */
132
+ cleanup(maxAgeMs: number = 7 * 24 * 60 * 60 * 1000): number {
133
+ const cutoff = Date.now() - maxAgeMs;
134
+ const result = this.#db.run(
135
+ "DELETE FROM async_tasks WHERE status IN ('completed', 'failed', 'cancelled') AND completed_at < ?",
136
+ [cutoff],
137
+ );
138
+ return result.changes;
139
+ }
140
+
141
+ count(): { total: number; pending: number; running: number; completed: number; failed: number } {
142
+ const row = this.#db
143
+ .prepare(
144
+ `SELECT
145
+ COUNT(*) as total,
146
+ SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
147
+ SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) as running,
148
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
149
+ SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed
150
+ FROM async_tasks`,
151
+ )
152
+ .get() as { total: number; pending: number; running: number; completed: number; failed: number } | null;
153
+ return row ?? { total: 0, pending: 0, running: 0, completed: 0, failed: 0 };
154
+ }
155
+
156
+ close(): void {
157
+ this.#db.close();
158
+ }
159
+ }
160
+
161
+ /** Singleton accessor */
162
+ let _instance: TaskStore | null = null;
163
+
164
+ export function getTaskStore(agentDir?: string): TaskStore {
165
+ if (!_instance) {
166
+ const dir = agentDir ?? `${process.env.HOME ?? "/tmp"}/.sns-myagent`;
167
+ _instance = new TaskStore(`${dir}/async/tasks.db`);
168
+ }
169
+ return _instance;
170
+ }
@@ -0,0 +1,70 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // Async Workflow Types
3
+ // ═══════════════════════════════════════════════════════════════════════════
4
+
5
+ /** Task execution status */
6
+ export type TaskStatus = "pending" | "running" | "completed" | "failed" | "cancelled";
7
+
8
+ /** Task action type */
9
+ export type TaskActionType = "prompt" | "shell" | "skill";
10
+
11
+ /** Task action — what to execute */
12
+ export type TaskAction =
13
+ | { type: "prompt"; prompt: string }
14
+ | { type: "shell"; command: string }
15
+ | { type: "skill"; skillName: string; args?: string };
16
+
17
+ /** Stored async task — domain model (camelCase) */
18
+ export interface AsyncTask {
19
+ id: string;
20
+ status: TaskStatus;
21
+ taskType: TaskActionType;
22
+ description: string;
23
+ result: string | null;
24
+ error: string | null;
25
+ createdAt: number;
26
+ startedAt: number | null;
27
+ completedAt: number | null;
28
+ metadata: Record<string, unknown> | null;
29
+ }
30
+
31
+ /** Row shape coming out of SQLite (snake_case) */
32
+ export interface AsyncTaskRow {
33
+ id: string;
34
+ status: string;
35
+ task_type: string;
36
+ description: string;
37
+ result: string | null;
38
+ error: string | null;
39
+ created_at: number;
40
+ started_at: number | null;
41
+ completed_at: number | null;
42
+ metadata: string | null;
43
+ }
44
+
45
+ /** Options for creating a new task */
46
+ export interface CreateTaskOptions {
47
+ description: string;
48
+ taskType?: TaskActionType;
49
+ metadata?: Record<string, unknown>;
50
+ }
51
+
52
+ /** Task execution result */
53
+ export interface TaskExecutionResult {
54
+ success: boolean;
55
+ result?: string;
56
+ error?: string;
57
+ }
58
+
59
+ /** Notification channel */
60
+ export type NotifyChannel = "cli" | "telegram";
61
+
62
+ /** Notification callback */
63
+ export type NotifyCallback = (task: AsyncTask, channel: NotifyChannel) => void | Promise<void>;
64
+
65
+ /** Task runner options */
66
+ export interface TaskRunnerOptions {
67
+ maxConcurrent?: number;
68
+ defaultTimeoutMs?: number;
69
+ pollIntervalMs?: number;
70
+ }
package/src/cli/entry.ts CHANGED
@@ -13,9 +13,11 @@ const head = argv[0];
13
13
 
14
14
  // Auto-boot the Telegram polling adapter when a token is present and the
15
15
  // user is not explicitly running a different subcommand. Disable with
16
- // SNS_TELEGRAM_AUTOSTART=0.
16
+ // SNS_TELEGRAM_AUTOSTART=0. Skip for "launch" which starts the full agent
17
+ // session and will wire the adapter after the session is ready.
17
18
  function maybeAutostartTelegram(): void {
18
19
  if (head === "telegram") return; // explicit start/stop handles its own lifecycle
20
+ if (head === "launch") return; // defer to runInteractiveMode in main.ts
19
21
  if (process.env.SNS_TELEGRAM_AUTOSTART === "0") return;
20
22
  const token = process.env.SNS_TELEGRAM_BOT_TOKEN;
21
23
  if (!token) return;
package/src/cli/index.ts CHANGED
@@ -15,6 +15,7 @@ import { loadConfig, saveConfig, configPath, ensureConfig } from "../config/load
15
15
  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
+ import { createForwardToAgent, resetChatSession, getBridgeStats } from "../adapters/telegram/bridge.js";
18
19
 
19
20
  // ---------- package version (single source of truth) ----------
20
21
 
@@ -74,63 +75,19 @@ function cmdInit(): number {
74
75
  }
75
76
  }
76
77
 
77
- function cmdChat(args: string[]): number {
78
- const stub = args.includes("--stub");
79
- process.stdout.write("snscoder chat — interactive mode\n");
80
- if (stub) {
81
- process.stdout.write("(stub mode: memory wiring arrives in Phase 2B)\n");
82
- }
78
+ async function cmdChat(_args: string[]): Promise<number> {
83
79
  const cfg = loadConfig();
84
- if (!cfg) {
85
- process.stdout.write("! no config found, run `snscoder init` first\n");
86
- process.stdout.write(" starting with in-memory defaults for this session\n");
87
- } else {
88
- process.stdout.write(`model: ${cfg.model.provider}/${cfg.model.model}\n`);
89
- process.stdout.write("type 'exit' to quit\n");
90
- }
91
- // Readline-style loop using readline from Node built-ins.
92
- return runReadlineLoop(cfg);
80
+ const { runEchoChat } = await import("../tui/chat-ui.js");
81
+ await runEchoChat({
82
+ model: cfg?.model.model,
83
+ provider: cfg?.model.provider,
84
+ version: PKG_VERSION,
85
+ agentName: cfg?.agentName ?? "SnsCoder",
86
+ });
87
+ return 0;
93
88
  }
94
89
 
95
- async function readLine(prompt: string): Promise<string> {
96
- const { createInterface } = await import("node:readline/promises");
97
- const { stdin, stdout } = await import("node:process");
98
- const rl = createInterface({ input: stdin, output: stdout });
99
- try {
100
- const answer = await rl.question(prompt);
101
- return answer;
102
- } finally {
103
- rl.close();
104
- }
105
- }
106
90
 
107
- function runReadlineLoop(cfg: Config | null): number {
108
- // Synchronous-ish loop using readline question/await via deasync-free path.
109
- // Keep it simple: each turn re-creates the interface so SIGINT cleanly closes it.
110
- const modelLabel = cfg ? `${cfg.model.provider}/${cfg.model.model}` : "defaults";
111
- process.stdout.write(`[${modelLabel}] ready.\n`);
112
- void (async () => {
113
- while (true) {
114
- let line: string;
115
- try {
116
- line = await readLine("you> ");
117
- } catch {
118
- process.stdout.write("\n");
119
- break;
120
- }
121
- const trimmed = line.trim();
122
- if (trimmed === "" || trimmed === "exit" || trimmed === "quit") {
123
- process.stdout.write("bye.\n");
124
- break;
125
- }
126
- process.stdout.write(`echo: ${trimmed}\n`);
127
- }
128
- // Explicit exit so the process actually terminates.
129
- process.exit(0);
130
- })();
131
- // Return a non-zero so Node keeps the event loop alive until readline ends.
132
- return 0;
133
- }
134
91
 
135
92
  function cmdConfigShow(cfg: Config): number {
136
93
  process.stdout.write(JSON.stringify(cfg, null, 2) + "\n");
@@ -268,7 +225,19 @@ async function cmdTelegram(args: string[]): Promise<number> {
268
225
  process.stderr.write(`✗ telegram start: ${probe.error}\n`);
269
226
  return 1;
270
227
  }
271
- const bot = startTelegramAdapter(token, { autostart: true });
228
+
229
+ // Wire the agent bridge — per-chat sessions via createAgentSession()
230
+ const agentForwarder = createForwardToAgent();
231
+ // Adapt 3-arg bridge to 2-arg handler signature: (text, sessionKey) → string
232
+ const forwardToAgent = (text: string, sessionKey: string) =>
233
+ agentForwarder(sessionKey, "telegram", text);
234
+
235
+ const bot = startTelegramAdapter(token, {
236
+ autostart: true,
237
+ forwardToAgent,
238
+ resetChatSession,
239
+ getBridgeStats,
240
+ });
272
241
  if (!bot) {
273
242
  process.stderr.write("✗ autostart refused by adapter\n");
274
243
  return 1;
@@ -366,10 +335,12 @@ Commands:
366
335
  version print package version
367
336
  init create .sns-myagent/config.json (defaults)
368
337
  chat [--stub] start interactive chat (stub for Phase 2B)
338
+ launch start full agent interactive mode
369
339
  config show print current config
370
340
  config get <key> read a dot-path value (e.g. model.provider)
371
341
  config set <key> <value> update a dot-path value
372
342
  telegram start|stop|status manage the Telegram polling adapter
343
+ orchestrate <prompt> multi-agent ensemble run (Phase 5)
373
344
  help print this help
374
345
  `;
375
346
 
@@ -394,6 +365,10 @@ export async function runCliAsync(argv: string[]): Promise<number> {
394
365
  return cmdConfig(rest);
395
366
  case "telegram":
396
367
  return cmdTelegram(rest);
368
+ case "launch":
369
+ return cmdLaunch(rest);
370
+ case "orchestrate":
371
+ return cmdOrchestrate(rest);
397
372
  default:
398
373
  process.stderr.write(`✗ unknown command: ${cmd}\n`);
399
374
  process.stderr.write(HELP);
@@ -401,6 +376,46 @@ export async function runCliAsync(argv: string[]): Promise<number> {
401
376
  }
402
377
  }
403
378
 
379
+ async function cmdOrchestrate(args: string[]): Promise<number> {
380
+ if (args.length === 0) {
381
+ process.stderr.write("✗ orchestrate requires a prompt\n");
382
+ process.stderr.write(" usage: snscoder orchestrate <prompt>\n");
383
+ process.stderr.write(" flags: --strategy consensus|critic|best_of_n\n");
384
+ process.stderr.write(" --agents role1,role2\n");
385
+ process.stderr.write(" --ensemble <name> (from agents.yaml)\n");
386
+ return 1;
387
+ }
388
+ const prompt = args.join(" ");
389
+ const opts: Record<string, unknown> = {};
390
+ for (let i = 0; i < args.length; i++) {
391
+ const a = args[i];
392
+ if (a === "--strategy") opts.strategy = args[++i];
393
+ else if (a === "--agents") opts.agents = (args[++i] ?? "").split(",").filter(Boolean);
394
+ else if (a === "--ensemble") opts.ensemble = args[++i];
395
+ }
396
+ void prompt; void opts; // parsed for future executor wiring (Task 5.1d)
397
+ try {
398
+ // CLI wrapper: no real LLM executor yet (Phase 5 stub).
399
+ // Until agent invocation is wired, surface a clear message instead of silently failing.
400
+ process.stderr.write(
401
+ "✗ orchestrate: agent executor not wired in CLI yet.\n" +
402
+ " Phase 5 ships the orchestrator module; the CLI integration lands once\n" +
403
+ " the LLM agent executor (src/agents/executor.ts) is implemented.\n" +
404
+ " For now use the ensemble module directly: import { executeEnsemble } from \"../agents/ensemble.js\".\n",
405
+ );
406
+ return 2;
407
+ } catch (err) {
408
+ process.stderr.write(`✗ orchestrate failed: ${(err as Error).message}\n`);
409
+ return 1;
410
+ }
411
+ }
412
+
413
+ async function cmdLaunch(args: string[]): Promise<number> {
414
+ const { main } = await import("../main.js");
415
+ await main(args);
416
+ return 0;
417
+ }
418
+
404
419
  export function runCli(argv: string[]): Promise<number> {
405
420
  return runCliAsync(argv);
406
421
  }
@@ -8,7 +8,7 @@
8
8
 
9
9
  import * as fs from "node:fs";
10
10
  import * as path from "node:path";
11
- import yaml from "js-yaml";
11
+ import * as yaml from "js-yaml";
12
12
  import {
13
13
  type SnsConfig,
14
14
  ensureConfigDir,
@@ -435,7 +435,7 @@ export class DebugSelectorComponent extends Container {
435
435
  if (!suppressed) {
436
436
  const sessionName = this.ctx.sessionManager.getSessionName();
437
437
  const notification: TerminalNotification = {
438
- title: sessionName || "Oh My Pi",
438
+ title: sessionName || "SnsCoder",
439
439
  body: "Terminal protocol test",
440
440
  type: "test",
441
441
  actions: "focus",
@@ -453,15 +453,22 @@ export class WelcomeComponent implements Component {
453
453
  }
454
454
  }
455
455
 
456
- export const PI_LOGO = ["▀██████████▀", " ╘██ ██ ", " ██ ██ ", " ██ ██ ", " ▄██▄ ▄██▄ "];
456
+ export const PI_LOGO = [
457
+ "███╗ ██╗███████╗██╗ ██╗██╗ ██╗███████╗",
458
+ "████╗ ██║██╔════╝╚██╗██╔╝██║ ██║██╔════╝",
459
+ "██╔██╗ ██║█████╗ ╚███╔╝ ██║ ██║███████╗",
460
+ "██║╚██╗██║██╔══╝ ██╔██╗ ██║ ██║╚════██║",
461
+ "██║ ╚████║███████╗██╔╝ ╚██╗╚██████╔╝███████║",
462
+ "╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝",
463
+ ];
457
464
 
458
465
  /** Multi-stop palette for the diagonal gradient. */
459
466
  const GRADIENT_STOPS: ReadonlyArray<readonly [number, number, number]> = [
460
- [255, 92, 200], // hot pink
461
- [200, 110, 255], // violet
462
- [120, 130, 255], // periwinkle
463
- [60, 200, 255], // bright cyan
464
- [120, 255, 220], // mint
467
+ [0, 210, 255], // #00d2ff — SNS cyan
468
+ [80, 150, 255], // transitional blue
469
+ [123, 47, 247], // #7b2ff7 — SNS purple
470
+ [200, 70, 180], // transitional pink-purple
471
+ [255, 107, 157], // #ff6b9d — SNS pink
465
472
  ];
466
473
 
467
474
  /** 256-color ramp fallback when truecolor isn't available. */
@@ -1182,7 +1182,7 @@ export class EventController {
1182
1182
 
1183
1183
  const sessionName = this.ctx.sessionManager.getSessionName();
1184
1184
  TERMINAL.sendNotification({
1185
- title: sessionName || "Oh My Pi",
1185
+ title: sessionName || "SnsCoder",
1186
1186
  body: "Complete",
1187
1187
  type: "completion",
1188
1188
  actions: "focus",
@@ -189,7 +189,7 @@ export function renderSetupSplash(width: number, height: number, elapsedMs: numb
189
189
  /** Centered fallback for windows too small to hold the full scene. */
190
190
  function renderCompactSplash(width: number, height: number, phase: number, shine: ShineConfig): string[] {
191
191
  const art = height >= 14 ? LARGE_LOGO : PI_LOGO;
192
- const content = [...gradientLogo(art, phase, shine), "", theme.bold("O h M y P i")];
192
+ const content = [...gradientLogo(art, phase, shine), "", theme.bold("S n s C o d e r")];
193
193
  const start = Math.max(0, Math.floor((height - content.length) / 2));
194
194
  const lines: string[] = [];
195
195
  for (let y = 0; y < height; y++) {
@@ -1376,7 +1376,7 @@ export class AgentSession {
1376
1376
  if (mode === "off") return;
1377
1377
  try {
1378
1378
  this.#powerAssertion = MacOSPowerAssertion.start({
1379
- reason: "Oh My Pi agent session",
1379
+ reason: "SnsCoder agent session",
1380
1380
  idle: true,
1381
1381
  display: mode === "display" || mode === "system",
1382
1382
  system: mode === "system",