@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.
- package/CHANGELOG.md +12 -1
- package/README.md +7 -8
- package/package.json +3 -3
- package/scripts/apply-pi-natives-patch.js +82 -56
- package/src/adapters/telegram/bot.ts +41 -1
- package/src/adapters/telegram/bridge.ts +186 -0
- package/src/adapters/telegram/handler.ts +138 -13
- package/src/adapters/telegram/index.ts +12 -2
- package/src/agents/__tests__/config.test.ts +79 -0
- package/src/agents/__tests__/resilience.test.ts +119 -0
- package/src/agents/__tests__/strategies.test.ts +83 -0
- package/src/agents/config.ts +367 -0
- package/src/agents/ensemble.ts +224 -0
- package/src/agents/resilience.ts +332 -0
- package/src/agents/strategies/best-of-n.ts +108 -0
- package/src/agents/strategies/consensus.ts +105 -0
- package/src/agents/strategies/critic.ts +131 -0
- package/src/agents/strategies/types.ts +68 -0
- package/src/async/__tests__/task-runner.test.ts +162 -0
- package/src/async/__tests__/task-store.test.ts +146 -0
- package/src/async/index.ts +28 -1
- package/src/async/notifier.ts +133 -0
- package/src/async/task-runner.ts +175 -0
- package/src/async/task-store.ts +170 -0
- package/src/async/types.ts +70 -0
- package/src/cli/entry.ts +3 -1
- package/src/cli/index.ts +69 -54
- package/src/config/index.ts +1 -1
- package/src/debug/index.ts +1 -1
- package/src/modes/components/welcome.ts +13 -6
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/setup-wizard/scenes/splash.ts +1 -1
- package/src/session/agent-session.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +63 -0
- package/src/slash-commands/helpers/task.ts +181 -0
- package/src/tbm/__tests__/tbm.test.ts +660 -0
- package/src/tbm/comm-modes.ts +165 -0
- package/src/tbm/config.ts +136 -0
- package/src/tbm/context-delta.ts +146 -0
- package/src/tbm/context-pyramid.ts +202 -0
- package/src/tbm/dashboard.ts +131 -0
- package/src/tbm/index.ts +247 -0
- package/src/tbm/lazy-skills.ts +182 -0
- package/src/tbm/response-cache.ts +220 -0
- package/src/tbm/tombstone.ts +189 -0
- package/src/tbm/tool-compress.ts +230 -0
- package/src/tools/ask.ts +1 -1
- package/src/tui/chat-blocks.ts +205 -0
- package/src/tui/chat-ui.ts +270 -0
- package/src/tui/code-cell.ts +90 -1
- package/src/tui/command-palette.ts +189 -0
- package/src/tui/index.ts +4 -0
- package/src/tui/splash.ts +130 -0
- package/src/ui/banner.ts +69 -29
- package/src/ui/colors.ts +24 -0
- package/src/ui/error-display.ts +130 -0
- package/src/ui/gradient.ts +104 -0
- package/src/ui/index.ts +15 -0
- package/src/ui/memory-toast.ts +102 -0
- package/src/ui/status-bar.ts +36 -30
- 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(
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/config/index.ts
CHANGED
package/src/debug/index.ts
CHANGED
|
@@ -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 || "
|
|
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
|
-
[
|
|
461
|
-
[
|
|
462
|
-
[
|
|
463
|
-
[
|
|
464
|
-
[
|
|
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 || "
|
|
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("
|
|
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: "
|
|
1379
|
+
reason: "SnsCoder agent session",
|
|
1380
1380
|
idle: true,
|
|
1381
1381
|
display: mode === "display" || mode === "system",
|
|
1382
1382
|
system: mode === "system",
|