@sns-myagent/cli 0.2.0 → 0.3.1
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/bin/.gitkeep +0 -0
- package/bin/snscoder.js +7 -96
- package/dist/cli.js +22026 -0
- package/package.json +4 -3
- package/scripts/apply-pi-natives-patch.js +82 -56
- package/scripts/fetch-binary.mjs +106 -217
- package/scripts/smoke-test.sh +92 -0
- 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 +230 -0
- package/src/agents/resilience.ts +332 -0
- package/src/agents/strategies/best-of-n.ts +108 -0
- package/src/agents/strategies/consensus.ts +104 -0
- package/src/agents/strategies/critic.ts +131 -0
- package/src/agents/strategies/index.ts +12 -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 +74 -55
- package/src/config/index.ts +1 -1
- package/src/debug/index.ts +1 -1
- package/src/internal-urls/docs-index.generated.txt +0 -2
- 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
|
@@ -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
|
|
|
@@ -33,7 +34,11 @@ import { fileURLToPath } from "node:url";
|
|
|
33
34
|
function readPackageVersion(): string {
|
|
34
35
|
const fromDefine = process.env.PKG_VERSION;
|
|
35
36
|
if (typeof fromDefine === "string" && fromDefine.length > 0) {
|
|
36
|
-
|
|
37
|
+
// Bun's --define injects the literal value as written. When the value
|
|
38
|
+
// is a JSON string (e.g. --define process.env.PKG_VERSION='"0.3.0"'),
|
|
39
|
+
// the surrounding quotes leak into process.env. Strip them.
|
|
40
|
+
const stripped = fromDefine.replace(/^["']|["']$/g, "");
|
|
41
|
+
if (stripped.length > 0) return stripped;
|
|
37
42
|
}
|
|
38
43
|
try {
|
|
39
44
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -74,63 +79,19 @@ function cmdInit(): number {
|
|
|
74
79
|
}
|
|
75
80
|
}
|
|
76
81
|
|
|
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
|
-
}
|
|
82
|
+
async function cmdChat(_args: string[]): Promise<number> {
|
|
83
83
|
const cfg = loadConfig();
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return runReadlineLoop(cfg);
|
|
84
|
+
const { runEchoChat } = await import("../tui/chat-ui.js");
|
|
85
|
+
await runEchoChat({
|
|
86
|
+
model: cfg?.model.model,
|
|
87
|
+
provider: cfg?.model.provider,
|
|
88
|
+
version: PKG_VERSION,
|
|
89
|
+
agentName: cfg?.agentName ?? "SnsCoder",
|
|
90
|
+
});
|
|
91
|
+
return 0;
|
|
93
92
|
}
|
|
94
93
|
|
|
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
94
|
|
|
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
95
|
|
|
135
96
|
function cmdConfigShow(cfg: Config): number {
|
|
136
97
|
process.stdout.write(JSON.stringify(cfg, null, 2) + "\n");
|
|
@@ -268,7 +229,19 @@ async function cmdTelegram(args: string[]): Promise<number> {
|
|
|
268
229
|
process.stderr.write(`✗ telegram start: ${probe.error}\n`);
|
|
269
230
|
return 1;
|
|
270
231
|
}
|
|
271
|
-
|
|
232
|
+
|
|
233
|
+
// Wire the agent bridge — per-chat sessions via createAgentSession()
|
|
234
|
+
const agentForwarder = createForwardToAgent();
|
|
235
|
+
// Adapt 3-arg bridge to 2-arg handler signature: (text, sessionKey) → string
|
|
236
|
+
const forwardToAgent = (text: string, sessionKey: string) =>
|
|
237
|
+
agentForwarder(sessionKey, "telegram", text);
|
|
238
|
+
|
|
239
|
+
const bot = startTelegramAdapter(token, {
|
|
240
|
+
autostart: true,
|
|
241
|
+
forwardToAgent,
|
|
242
|
+
resetChatSession,
|
|
243
|
+
getBridgeStats,
|
|
244
|
+
});
|
|
272
245
|
if (!bot) {
|
|
273
246
|
process.stderr.write("✗ autostart refused by adapter\n");
|
|
274
247
|
return 1;
|
|
@@ -366,10 +339,12 @@ Commands:
|
|
|
366
339
|
version print package version
|
|
367
340
|
init create .sns-myagent/config.json (defaults)
|
|
368
341
|
chat [--stub] start interactive chat (stub for Phase 2B)
|
|
342
|
+
launch start full agent interactive mode
|
|
369
343
|
config show print current config
|
|
370
344
|
config get <key> read a dot-path value (e.g. model.provider)
|
|
371
345
|
config set <key> <value> update a dot-path value
|
|
372
346
|
telegram start|stop|status manage the Telegram polling adapter
|
|
347
|
+
orchestrate <prompt> multi-agent ensemble run (Phase 5)
|
|
373
348
|
help print this help
|
|
374
349
|
`;
|
|
375
350
|
|
|
@@ -394,6 +369,10 @@ export async function runCliAsync(argv: string[]): Promise<number> {
|
|
|
394
369
|
return cmdConfig(rest);
|
|
395
370
|
case "telegram":
|
|
396
371
|
return cmdTelegram(rest);
|
|
372
|
+
case "launch":
|
|
373
|
+
return cmdLaunch(rest);
|
|
374
|
+
case "orchestrate":
|
|
375
|
+
return cmdOrchestrate(rest);
|
|
397
376
|
default:
|
|
398
377
|
process.stderr.write(`✗ unknown command: ${cmd}\n`);
|
|
399
378
|
process.stderr.write(HELP);
|
|
@@ -401,6 +380,46 @@ export async function runCliAsync(argv: string[]): Promise<number> {
|
|
|
401
380
|
}
|
|
402
381
|
}
|
|
403
382
|
|
|
383
|
+
async function cmdOrchestrate(args: string[]): Promise<number> {
|
|
384
|
+
if (args.length === 0) {
|
|
385
|
+
process.stderr.write("✗ orchestrate requires a prompt\n");
|
|
386
|
+
process.stderr.write(" usage: snscoder orchestrate <prompt>\n");
|
|
387
|
+
process.stderr.write(" flags: --strategy consensus|critic|best_of_n\n");
|
|
388
|
+
process.stderr.write(" --agents role1,role2\n");
|
|
389
|
+
process.stderr.write(" --ensemble <name> (from agents.yaml)\n");
|
|
390
|
+
return 1;
|
|
391
|
+
}
|
|
392
|
+
const prompt = args.join(" ");
|
|
393
|
+
const opts: Record<string, unknown> = {};
|
|
394
|
+
for (let i = 0; i < args.length; i++) {
|
|
395
|
+
const a = args[i];
|
|
396
|
+
if (a === "--strategy") opts.strategy = args[++i];
|
|
397
|
+
else if (a === "--agents") opts.agents = (args[++i] ?? "").split(",").filter(Boolean);
|
|
398
|
+
else if (a === "--ensemble") opts.ensemble = args[++i];
|
|
399
|
+
}
|
|
400
|
+
void prompt; void opts; // parsed for future executor wiring (Task 5.1d)
|
|
401
|
+
try {
|
|
402
|
+
// CLI wrapper: no real LLM executor yet (Phase 5 stub).
|
|
403
|
+
// Until agent invocation is wired, surface a clear message instead of silently failing.
|
|
404
|
+
process.stderr.write(
|
|
405
|
+
"✗ orchestrate: agent executor not wired in CLI yet.\n" +
|
|
406
|
+
" Phase 5 ships the orchestrator module; the CLI integration lands once\n" +
|
|
407
|
+
" the LLM agent executor (src/agents/executor.ts) is implemented.\n" +
|
|
408
|
+
" For now use the ensemble module directly: import { executeEnsemble } from \"../agents/ensemble.js\".\n",
|
|
409
|
+
);
|
|
410
|
+
return 2;
|
|
411
|
+
} catch (err) {
|
|
412
|
+
process.stderr.write(`✗ orchestrate failed: ${(err as Error).message}\n`);
|
|
413
|
+
return 1;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function cmdLaunch(args: string[]): Promise<number> {
|
|
418
|
+
const { main } = await import("../main.js");
|
|
419
|
+
await main(args);
|
|
420
|
+
return 0;
|
|
421
|
+
}
|
|
422
|
+
|
|
404
423
|
export function runCli(argv: string[]): Promise<number> {
|
|
405
424
|
return runCliAsync(argv);
|
|
406
425
|
}
|
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",
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
["configuration.md","faq.md","installation.md","memory.md","tbm.md","terminal-ui.md","troubleshooting.md"]
|
|
2
|
-
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
|