@sns-myagent/cli 0.3.7 → 0.3.9

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 (70) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +63 -24
  3. package/bin/snsagent.js +3 -41
  4. package/dist/cli.js +3267 -3288
  5. package/package.json +3 -3
  6. package/src/adapters/telegram/handler.ts +1 -1
  7. package/src/agents/ensemble.ts +3 -4
  8. package/src/agents/executor.ts +129 -0
  9. package/src/async/notifier.ts +1 -1
  10. package/src/cli/config-cli.ts +1 -1
  11. package/src/cli/entry.ts +2 -2
  12. package/src/cli/gallery-fixtures/misc.ts +3 -3
  13. package/src/cli/grep-cli.ts +3 -3
  14. package/src/cli/grievances-cli.ts +2 -2
  15. package/src/cli/index.ts +243 -24
  16. package/src/cli/plugin-cli.ts +5 -5
  17. package/src/cli/setup-cli.ts +3 -3
  18. package/src/cli/setup-model-picker.ts +1 -1
  19. package/src/cli/shell-cli.ts +1 -1
  20. package/src/cli/ssh-cli.ts +1 -1
  21. package/src/cli/stats-cli.ts +1 -1
  22. package/src/cli/tiny-models-cli.ts +3 -3
  23. package/src/cli/ttsr-cli.ts +2 -2
  24. package/src/cli/update-cli.ts +3 -3
  25. package/src/cli/usage-cli.ts +1 -1
  26. package/src/cli-commands.ts +6 -6
  27. package/src/commands/acp.ts +2 -2
  28. package/src/commands/chat.ts +3 -3
  29. package/src/commands/install.ts +5 -5
  30. package/src/commands/say.ts +4 -4
  31. package/src/commands/setup.ts +1 -1
  32. package/src/config/defaults.ts +5 -3
  33. package/src/config/schema.ts +8 -0
  34. package/src/config/settings-schema.ts +1 -1
  35. package/src/config/sns-config.ts +1 -1
  36. package/src/extensibility/plugins/loader.ts +1 -1
  37. package/src/extensibility/plugins/manager.ts +2 -2
  38. package/src/hindsight/client.ts +1 -1
  39. package/src/internal-urls/docs-index.generated.txt +2 -0
  40. package/src/main.ts +4 -0
  41. package/src/modes/acp/acp-agent.ts +3 -3
  42. package/src/modes/components/plugin-settings.ts +2 -2
  43. package/src/modes/components/welcome.ts +66 -160
  44. package/src/modes/interactive-mode.ts +2 -2
  45. package/src/modes/print-mode.ts +2 -2
  46. package/src/modes/setup-version.ts +1 -1
  47. package/src/modes/setup-wizard/scenes/byok-setup.ts +354 -0
  48. package/src/modes/setup-wizard/scenes/outro.ts +2 -2
  49. package/src/modes/setup-wizard/scenes/providers.ts +6 -5
  50. package/src/modes/setup-wizard/scenes/splash.ts +3 -3
  51. package/src/modes/setup-wizard/wizard-overlay.ts +2 -2
  52. package/src/modes/theme/dark.json +21 -20
  53. package/src/modes/theme/light.json +30 -29
  54. package/src/modes/utils/ui-helpers.ts +1 -1
  55. package/src/slash-commands/builtin-registry.ts +6 -3
  56. package/src/stt/recorder.ts +3 -3
  57. package/src/tts/player.ts +1 -1
  58. package/src/tts/tts-client.ts +1 -1
  59. package/src/tui/chat-blocks.ts +4 -4
  60. package/src/tui/chat-ui.ts +3 -3
  61. package/src/tui/code-cell.ts +3 -3
  62. package/src/tui/command-palette.ts +3 -3
  63. package/src/tui/splash.ts +12 -18
  64. package/src/ui/banner.ts +51 -66
  65. package/src/ui/chat-prompt.ts +2 -2
  66. package/src/ui/colors.ts +17 -14
  67. package/src/ui/error-display.ts +2 -2
  68. package/src/ui/gradient.ts +3 -3
  69. package/src/ui/memory-toast.ts +5 -5
  70. package/src/ui/status-bar.ts +1 -1
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@sns-myagent/cli",
4
- "version": "0.3.7",
5
- "description": "SNS-MyAgent \u2014 BYOK coding agent CLI. Multi-agent + Telegram + self-learning memory. ~120MB binary, zero forced cost.",
4
+ "version": "0.3.9",
5
+ "description": "SNS-MyAgent 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",
8
8
  "contributors": [],
@@ -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.4\"' --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",
38
+ "build": "bun build --compile --define process.env.PI_COMPILED='\"true\"' --define process.env.PKG_VERSION='\"0.3.9\"' --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 .",
@@ -122,7 +122,7 @@ export function parseMessage(msg: Message): ParsedMessage {
122
122
  */
123
123
  export const COMMAND_REPLIES: Record<TelegramCommand, string> = {
124
124
  start:
125
- "*Welcome to snscoder* 🤖\n\n" +
125
+ "*Welcome to snsagent* 🤖\n\n" +
126
126
  "I'm a Telegram front-end for the SNS-MyAgent coding agent. " +
127
127
  "Send me any task or question; I'll route it to the agent and reply here.\n\n" +
128
128
  "Type /help for the full command list.",
@@ -62,7 +62,7 @@ export async function executeEnsemble(
62
62
 
63
63
  // Execute with resilience if enabled
64
64
  const executeWithResilience = options.resilient !== false
65
- ? createResilientExecutor(config, options)
65
+ ? createResilientExecutor(config, options, executeAgent)
66
66
  : executeAgent;
67
67
 
68
68
  const result = await strategy.execute(prompt, agentRoles, executeWithResilience);
@@ -147,6 +147,7 @@ function createStrategy(name: string, options: Record<string, unknown>): Ensembl
147
147
  function createResilientExecutor(
148
148
  config: ReturnType<typeof getAgentsConfig>["config"],
149
149
  options: EnsembleOptions,
150
+ executeAgent: (role: string, prompt: string) => Promise<AgentResponse>,
150
151
  ) {
151
152
  return async (role: string, prompt: string): Promise<AgentResponse> => {
152
153
  const agentConfig = config.agents[role];
@@ -158,8 +159,7 @@ function createResilientExecutor(
158
159
  async () => {
159
160
  return resilience.withTimeout(
160
161
  async () => {
161
- // This will be replaced by actual agent execution via the wrapper
162
- throw new Error("executeAgent must be provided by caller");
162
+ return executeAgent(role, prompt);
163
163
  },
164
164
  options.taskTimeoutMs ?? config.task_timeout_ms ?? 120_000,
165
165
  `ensemble:${role}`,
@@ -171,7 +171,6 @@ function createResilientExecutor(
171
171
  },
172
172
  );
173
173
  if (!result.success) throw result.error ?? new Error("resilient executor failed");
174
- // Type guard: caller must inject a real AgentResponse executor.
175
174
  return result.result as unknown as AgentResponse;
176
175
  };
177
176
  }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Ensemble Agent Executor — SNS-MyAgent Phase 5.1d
3
+ *
4
+ * Bridges the ensemble orchestrator (src/agents/ensemble.ts) to the
5
+ * actual LLM execution (src/task/executor.ts).
6
+ *
7
+ * Provides `executeAgentForEnsemble(role, prompt) → AgentResponse`
8
+ * which resolves agent config from agents.yaml, builds an AgentDefinition,
9
+ * calls runSubprocess, and returns the typed response.
10
+ */
11
+
12
+ import { randomBytes } from "node:crypto";
13
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
14
+ import { getAgentsConfig, type AgentRoleConfig } from "./config.js";
15
+ import { runSubprocess, type ExecutorOptions } from "../task/executor.js";
16
+ import type { AgentDefinition, SingleResult } from "../task/types.js";
17
+ import type { AgentResponse } from "./strategies/types.js";
18
+
19
+ // ─── Default agent definition ───────────────────────────────────────────────
20
+
21
+ const DEFAULT_SYSTEM_PROMPT = `You are a helpful AI assistant. Answer the user's request directly and concisely.
22
+ Do not use any tools. Do not spawn subagents. Just answer.`;
23
+
24
+ const DEFAULT_AGENT: AgentDefinition = {
25
+ name: "task",
26
+ description: "General-purpose agent for ensemble tasks",
27
+ systemPrompt: DEFAULT_SYSTEM_PROMPT,
28
+ source: "bundled",
29
+ };
30
+
31
+ // ─── Executor ───────────────────────────────────────────────────────────────
32
+
33
+ /**
34
+ * Execute a single agent call for the ensemble orchestrator.
35
+ *
36
+ * @param role - Agent role name (from agents.yaml or "task")
37
+ * @param prompt - The user prompt to send
38
+ * @returns AgentResponse compatible with ensemble strategies
39
+ */
40
+ export async function executeAgentForEnsemble(
41
+ role: string,
42
+ prompt: string,
43
+ ): Promise<AgentResponse> {
44
+ const startTime = Date.now();
45
+ const config = getAgentsConfig();
46
+ const roleConfig = config.resolveRole(role);
47
+
48
+ // Build AgentDefinition from config (or use default)
49
+ const agentDef = buildAgentDefinition(role, roleConfig);
50
+
51
+ // Resolve model string for display
52
+ const modelStr = roleConfig?.model ?? "default";
53
+
54
+ // Generate unique ID for this execution
55
+ const execId = `ensemble-${role}-${randomBytes(4).toString("hex")}`;
56
+
57
+ // Build executor options
58
+ const cwd = process.cwd();
59
+ const options: ExecutorOptions = {
60
+ cwd,
61
+ agent: agentDef,
62
+ task: prompt,
63
+ index: 0,
64
+ id: execId,
65
+ role,
66
+ // Use model override from agents.yaml if specified
67
+ modelOverride: roleConfig?.model,
68
+ thinkingLevel: resolveThinkingLevel(roleConfig?.thinking_level),
69
+ };
70
+
71
+ try {
72
+ const result: SingleResult = await runSubprocess(options);
73
+
74
+ return {
75
+ role,
76
+ model: result.resolvedModel ?? modelStr,
77
+ content: result.output || result.error || "(no output)",
78
+ tokens: result.usage
79
+ ? { input: result.usage.input ?? 0, output: result.usage.output ?? 0 }
80
+ : undefined,
81
+ timeMs: Date.now() - startTime,
82
+ };
83
+ } catch (err) {
84
+ const msg = err instanceof Error ? err.message : String(err);
85
+ return {
86
+ role,
87
+ model: modelStr,
88
+ content: `[executor error] ${msg}`,
89
+ timeMs: Date.now() - startTime,
90
+ };
91
+ }
92
+ }
93
+
94
+ // ─── Helpers ────────────────────────────────────────────────────────────────
95
+
96
+ function buildAgentDefinition(
97
+ role: string,
98
+ roleConfig: AgentRoleConfig | null,
99
+ ): AgentDefinition {
100
+ return {
101
+ name: role,
102
+ description: roleConfig?.system_prompt
103
+ ? `Custom agent: ${role}`
104
+ : `Ensemble agent: ${role}`,
105
+ systemPrompt: roleConfig?.system_prompt ?? DEFAULT_SYSTEM_PROMPT,
106
+ tools: roleConfig?.tools,
107
+ source: roleConfig ? "user" : "bundled",
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Map a string thinking_level from agents.yaml to the actual ThinkingLevel const.
113
+ * Returns undefined if not set or unrecognized.
114
+ */
115
+ function resolveThinkingLevel(
116
+ level: string | undefined,
117
+ ): ThinkingLevel | undefined {
118
+ if (!level) return undefined;
119
+ const map: Record<string, ThinkingLevel> = {
120
+ inherit: "inherit" as ThinkingLevel,
121
+ off: "off" as ThinkingLevel,
122
+ minimal: "minimal" as ThinkingLevel,
123
+ low: "low" as ThinkingLevel,
124
+ medium: "medium" as ThinkingLevel,
125
+ high: "high" as ThinkingLevel,
126
+ xhigh: "xhigh" as ThinkingLevel,
127
+ };
128
+ return map[level.toLowerCase()] ?? undefined;
129
+ }
@@ -10,7 +10,7 @@ const STATUS_COLORS: Record<string, (text: string) => string> = {
10
10
  completed: (t) => chalk.green(t),
11
11
  failed: (t) => chalk.red(t),
12
12
  cancelled: (t) => chalk.yellow(t),
13
- running: (t) => chalk.cyan(t),
13
+ running: (t) => chalk.hex("#F97316")(t),
14
14
  pending: (t) => chalk.dim(t),
15
15
  };
16
16
 
@@ -132,7 +132,7 @@ function formatValue(value: unknown): string {
132
132
  return value ? chalk.green("true") : chalk.red("false");
133
133
  }
134
134
  if (typeof value === "number") {
135
- return chalk.cyan(String(value));
135
+ return chalk.hex("#F97316")(String(value));
136
136
  }
137
137
  if (typeof value === "string") {
138
138
  return chalk.yellow(value);
package/src/cli/entry.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  // Single-file CLI entry for `bun build --compile`.
3
- // Mirrors bin/snscoder (Node dev shim) but is a TS module so Bun can bundle
3
+ // Mirrors bin/snsagent (Node dev shim) but is a TS module so Bun can bundle
4
4
  // all transitive imports into the binary.
5
5
 
6
6
  import { ensureConfig } from "../config/loader.js";
@@ -34,7 +34,7 @@ try {
34
34
  } catch (err) {
35
35
  const e = err as Error;
36
36
  const msg = e.message ? e.message : String(err);
37
- process.stderr.write(`\u2717 snscoder crashed: ${msg}\n`);
37
+ process.stderr.write(`\u2717 snsagent crashed: ${msg}\n`);
38
38
  if (process.env.SNS_DEBUG) {
39
39
  process.stderr.write("\n" + (e.stack ?? "") + "\n");
40
40
  }
@@ -119,14 +119,14 @@ export const miscFixtures: Record<string, GalleryFixture> = {
119
119
  {
120
120
  type: "text",
121
121
  text: [
122
- "● omp-api.service - Oh My Pi API",
123
- " Loaded: loaded (/etc/systemd/system/omp-api.service; enabled)",
122
+ "● snsagent-api.service - snsagent API",
123
+ " Loaded: loaded (/etc/systemd/system/snsagent-api.service; enabled)",
124
124
  " Active: active (running) since Sat 2026-06-06 09:14:02 UTC; 3h 21min ago",
125
125
  " Main PID: 4812 (bun)",
126
126
  " Tasks: 17 (limit: 4915)",
127
127
  " Memory: 142.6M",
128
128
  " CPU: 38.214s",
129
- " CGroup: /system.slice/omp-api.service",
129
+ " CGroup: /system.slice/snsagent-api.service",
130
130
  " └─4812 /usr/local/bin/bun run dist/server.js",
131
131
  ].join("\n"),
132
132
  },
@@ -111,7 +111,7 @@ export async function runGrepCommand(cmd: GrepCommandArgs): Promise<void> {
111
111
  console.log(chalk.dim(`${displayPath}-${ctx.lineNumber}- ${ctx.line}`));
112
112
  }
113
113
  }
114
- console.log(`${chalk.cyan(displayPath)}:${chalk.yellow(String(match.lineNumber))}: ${match.line}`);
114
+ console.log(`${chalk.hex("#F97316")(displayPath)}:${chalk.yellow(String(match.lineNumber))}: ${match.line}`);
115
115
  if (match.contextAfter) {
116
116
  for (const ctx of match.contextAfter) {
117
117
  console.log(chalk.dim(`${displayPath}-${ctx.lineNumber}- ${ctx.line}`));
@@ -119,9 +119,9 @@ export async function runGrepCommand(cmd: GrepCommandArgs): Promise<void> {
119
119
  }
120
120
  console.log("");
121
121
  } else if (cmd.mode === GrepOutputMode.Count) {
122
- console.log(`${chalk.cyan(displayPath)}: ${match.matchCount ?? 0} matches`);
122
+ console.log(`${chalk.hex("#F97316")(displayPath)}: ${match.matchCount ?? 0} matches`);
123
123
  } else {
124
- console.log(chalk.cyan(displayPath));
124
+ console.log(chalk.hex("#F97316")(displayPath));
125
125
  }
126
126
  }
127
127
  } catch (err) {
@@ -71,7 +71,7 @@ export async function listGrievances(options: ListGrievancesOptions): Promise<vo
71
71
 
72
72
  for (const row of rows) {
73
73
  console.log(
74
- `${chalk.dim(`#${row.id}`)} ${chalk.cyan(row.tool)} ${chalk.dim(`(${row.model} v${row.version})`)}`,
74
+ `${chalk.dim(`#${row.id}`)} ${chalk.hex("#F97316")(row.tool)} ${chalk.dim(`(${row.model} v${row.version})`)}`,
75
75
  );
76
76
  console.log(` ${row.report}`);
77
77
  console.log();
@@ -181,7 +181,7 @@ function makeProgressBar(total: number, width = 30): ProgressBar {
181
181
  const pct = `${Math.floor(ratio * 100)
182
182
  .toString()
183
183
  .padStart(3, " ")}%`;
184
- process.stdout.write(`\r${chalk.cyan("Pushing")} [${bar}] ${pct} ${done}/${total}`);
184
+ process.stdout.write(`\r${chalk.hex("#F97316")("Pushing")} [${bar}] ${pct} ${done}/${total}`);
185
185
  };
186
186
  render(0);
187
187
  return {
package/src/cli/index.ts CHANGED
@@ -14,15 +14,20 @@
14
14
  import { loadConfig, saveConfig, configPath, ensureConfig } from "../config/loader.js";
15
15
  import type { Config } from "../config/schema.js";
16
16
  import { defaultConfigFn } from "../config/loader.js";
17
+ import { getAgentDir } from "@oh-my-pi/pi-utils";
17
18
  import { startTelegramAdapter, stopTelegramAdapter } from "../adapters/telegram/index.js";
18
19
  import { createForwardToAgent, resetChatSession, getBridgeStats } from "../adapters/telegram/bridge.js";
20
+ import { createInterface } from "node:readline/promises";
21
+ import { stdin, stdout } from "node:process";
22
+ import chalk from "chalk";
19
23
 
20
24
  // ---------- package version (single source of truth) ----------
21
25
 
22
26
  // Inline version — kept in sync with package.json. Read from package.json at
23
27
  // runtime so the bin script does not need a second copy.
24
- import { readFileSync } from "node:fs";
25
- import { dirname, resolve } from "node:path";
28
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
29
+ import { homedir } from "node:os";
30
+ import { dirname, join, resolve } from "node:path";
26
31
  import { fileURLToPath } from "node:url";
27
32
 
28
33
  /**
@@ -61,17 +66,206 @@ function cmdVersion(): number {
61
66
  return 0;
62
67
  }
63
68
 
64
- function cmdInit(): number {
69
+ /** Derive a provider name from base URL for models.yml key. */
70
+ function deriveProviderName(baseUrl: string): string {
71
+ try {
72
+ const url = new URL(baseUrl);
73
+ return url.hostname.replace(/^api\./, "").replace(/\./g, "-") || "custom-provider";
74
+ } catch {
75
+ return "custom-provider";
76
+ }
77
+ }
78
+
79
+ /** BYOK provider setup — reusable for first-run and re-init. */
80
+ async function byokProviderSetup(rl: ReturnType<typeof createInterface>): Promise<void> {
81
+ process.stdout.write("\n");
82
+ process.stdout.write(` ${chalk.hex("#F97316")("●")} ${chalk.bold("AI Provider Setup")}\n`);
83
+ process.stdout.write(` ${chalk.dim("Connect your LLM provider (skip to set via env vars later):")}\n`);
84
+
85
+ // Base URL
86
+ process.stdout.write(` ${chalk.hex("#F97316")("●")} Base URL [https://api.openai.com/v1]: `);
87
+ const baseUrlRaw = await rl.question("");
88
+ const baseUrl = (baseUrlRaw.trim() || "https://api.openai.com/v1").replace(/\/+$/, "");
89
+
90
+ // API Key
91
+ process.stdout.write(` ${chalk.hex("#F97316")("●")} API Key: `);
92
+ const apiKey = await rl.question("");
93
+
94
+ // API type
95
+ process.stdout.write(` ${chalk.hex("#F97316")("●")} API Type:\n`);
96
+ process.stdout.write(` ${chalk.hex("#F97316")("1")} openai-completions (default — OpenAI, OpenRouter, Ollama, vLLM)\n`);
97
+ process.stdout.write(` ${chalk.hex("#F97316")("2")} openai-responses\n`);
98
+ process.stdout.write(` ${chalk.hex("#F97316")("3")} anthropic-messages\n`);
99
+ process.stdout.write(` ${chalk.hex("#F97316")("4")} google-generative-ai\n`);
100
+ process.stdout.write(` ${chalk.hex("#F97316")("5")} azure-openai-responses\n`);
101
+ process.stdout.write(` [1] `);
102
+ const apiTypeAnswer = await rl.question("");
103
+
104
+ const apiTypeMap: Record<string, string> = {
105
+ "1": "openai-completions",
106
+ "2": "openai-responses",
107
+ "3": "anthropic-messages",
108
+ "4": "google-generative-ai",
109
+ "5": "azure-openai-responses",
110
+ };
111
+ const apiType = apiTypeMap[apiTypeAnswer.trim()] || "openai-completions";
112
+
113
+ // Save provider to models.yml
114
+ const apiKeyTrimmed = apiKey.trim();
115
+ if (apiKeyTrimmed || baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1")) {
116
+ try {
117
+ const { YAML } = await import("bun");
118
+ const agentDir = getAgentDir();
119
+ const modelsPath = join(agentDir, "models.yml");
120
+
121
+ let existing: Record<string, unknown> = {};
122
+ if (existsSync(modelsPath)) {
123
+ try {
124
+ const content = readFileSync(modelsPath, "utf-8");
125
+ existing = (YAML.parse(content) as Record<string, unknown>) ?? {};
126
+ } catch { existing = {}; }
127
+ }
128
+
129
+ const providers = (existing.providers ?? {}) as Record<string, unknown>;
130
+ const providerName = deriveProviderName(baseUrl);
131
+
132
+ // Determine auth mode — no API key + localhost = auth "none"
133
+ const isNoAuth = !apiKeyTrimmed;
134
+ const providerConfig: Record<string, unknown> = {
135
+ baseUrl,
136
+ api: apiType,
137
+ auth: isNoAuth ? "none" : "apiKey",
138
+ };
139
+ if (apiKeyTrimmed) providerConfig.apiKey = apiKeyTrimmed;
140
+
141
+ // Auto-detect models for OpenAI-compatible
142
+ if (!isNoAuth && (apiType === "openai-completions" || apiType === "openai-responses")) {
143
+ process.stdout.write(` ${chalk.dim("Detecting models…")}\n`);
144
+ try {
145
+ const headers: Record<string, string> = {};
146
+ if (apiKeyTrimmed) headers["Authorization"] = `Bearer ${apiKeyTrimmed}`;
147
+ const res = await fetch(`${baseUrl}/models`, { headers, signal: AbortSignal.timeout(10_000) });
148
+ if (res.ok) {
149
+ const data = (await res.json()) as { data?: { id: string }[] };
150
+ if (data.data?.length) {
151
+ const models = data.data.map(m => m.id).filter(Boolean).sort();
152
+ providerConfig.models = models.map(id => ({
153
+ id,
154
+ api: apiType,
155
+ contextWindow: 128000,
156
+ supportsTools: true,
157
+ }));
158
+ process.stdout.write(` ${chalk.green("✓")} ${models.length} model${models.length !== 1 ? "s" : ""} detected\n`);
159
+ } else {
160
+ process.stdout.write(` ${chalk.dim("No models listed at /models endpoint (provider may not support it)")}\n`);
161
+ }
162
+ } else {
163
+ process.stdout.write(` ${chalk.dim(`Could not detect models (HTTP ${res.status}) — manual config may be needed`)}\n`);
164
+ }
165
+ } catch {
166
+ process.stdout.write(` ${chalk.dim("Could not reach /models endpoint — saved without model list")}\n`);
167
+ }
168
+ }
169
+
170
+ providers[providerName] = providerConfig;
171
+ existing.providers = providers;
172
+
173
+ if (!existsSync(agentDir)) mkdirSync(agentDir, { recursive: true });
174
+ writeFileSync(modelsPath, YAML.stringify(existing), "utf-8");
175
+ process.stdout.write(` ${chalk.green("✓")} Provider saved to ${modelsPath}\n`);
176
+ } catch (err) {
177
+ process.stdout.write(` ${chalk.dim(`Could not save provider config: ${(err as Error).message}`)}\n`);
178
+ }
179
+ } else {
180
+ process.stdout.write(` ${chalk.dim("Skipped — set OPENAI_API_KEY or ANTHROPIC_API_KEY env var to connect later")}\n`);
181
+ }
182
+ }
183
+
184
+ async function cmdInit(): Promise<number> {
65
185
  const path = configPath();
66
186
  try {
67
187
  const existing = loadConfig();
68
188
  if (existing) {
69
189
  process.stdout.write(`✓ config already exists at ${path}\n`);
190
+ // Still allow BYOK provider re-configuration
191
+ process.stdout.write(` ${chalk.dim("Re-run provider setup? [y/N] ")}`);
192
+ const rl = createInterface({ input: stdin, output: stdout });
193
+ const reconfigure = await rl.question("");
194
+ if (reconfigure.trim().toLowerCase() !== "y") {
195
+ rl.close();
196
+ return 0;
197
+ }
198
+ await byokProviderSetup(rl);
199
+ rl.close();
70
200
  return 0;
71
201
  }
202
+
203
+ // Interactive memory backend selection
204
+ const rl = createInterface({ input: stdin, output: stdout });
205
+
206
+ process.stdout.write("\n");
207
+ process.stdout.write(` ${chalk.hex("#F97316")("●")} ${chalk.bold("Memory Backend Selection")}\n`);
208
+ process.stdout.write(` ${chalk.dim("Select memory backend (default: mnemopi):")}\n`);
209
+ process.stdout.write(` ${chalk.hex("#F97316")("1")} mnemopi (default) — SQLite + vector + graph, offline, no config\n`);
210
+ process.stdout.write(` ${chalk.hex("#F97316")("2")} local — rollout summary only\n`);
211
+ process.stdout.write(` ${chalk.hex("#F97316")("3")} mnemosyne — advanced local (Python daemon)\n`);
212
+ process.stdout.write(` ${chalk.hex("#F97316")("4")} mem0 — cloud API (needs MEM0_API_KEY)\n`);
213
+ process.stdout.write(` ${chalk.hex("#F97316")("5")} lcm — local server (needs LCM_HOST)\n`);
214
+ process.stdout.write(` ${chalk.hex("#F97316")("6")} hindsight — cloud (needs HINDSIGHT_API_KEY)\n`);
215
+ process.stdout.write(` ${chalk.hex("#F97316")("7")} off — disabled\n`);
216
+ process.stdout.write(` [1] `);
217
+
218
+ const answer = await rl.question("");
219
+
220
+ let backend: "mnemopi" | "hindsight" | "mnemosyne" | "mem0" | "lcm" | "local" | "off" = "mnemopi";
221
+ switch (answer.trim()) {
222
+ case "1":
223
+ case "":
224
+ backend = "mnemopi";
225
+ break;
226
+ case "2":
227
+ backend = "local";
228
+ break;
229
+ case "3":
230
+ backend = "mnemosyne";
231
+ break;
232
+ case "4":
233
+ backend = "mem0";
234
+ break;
235
+ case "5":
236
+ backend = "lcm";
237
+ break;
238
+ case "6":
239
+ backend = "hindsight";
240
+ break;
241
+ case "7":
242
+ backend = "off";
243
+ break;
244
+ default:
245
+ process.stdout.write(` ${chalk.dim("unknown option, using default (mnemopi)")}\n`);
246
+ backend = "mnemopi";
247
+ }
248
+
249
+ // Mnemosyne needs Python daemon
250
+ if (backend === "mnemosyne") {
251
+ process.stdout.write(`\n ${chalk.hex("#F97316")("●")} mnemosyne requires Python daemon.\n`);
252
+ process.stdout.write(` ${chalk.dim("Install now? (pip install mnemosyne) [Y/n] ")}`);
253
+ const yn = await rl.question("");
254
+ if (!yn || yn.toLowerCase() === "y") {
255
+ process.stdout.write(` ${chalk.dim("installing mnemosyne... (placeholder)")}\n`);
256
+ // TODO: actual pip install
257
+ }
258
+ }
259
+
260
+ // ── BYOK Provider Setup ─────────────────────────────────────
261
+ await byokProviderSetup(rl);
262
+
263
+ rl.close();
264
+
72
265
  const cfg = defaultConfigFn();
266
+ cfg.memory = { ...cfg.memory, backend };
73
267
  saveConfig(cfg);
74
- process.stdout.write(`✓ created ${path}\n`);
268
+ process.stdout.write(`✓ created ${path} (memory backend: ${backend})\n`);
75
269
  return 0;
76
270
  } catch (err) {
77
271
  process.stderr.write(`✗ init failed: ${(err as Error).message}\n`);
@@ -341,14 +535,15 @@ Commands:
341
535
  (none) start full agent interactive mode (default)
342
536
  agent alias for default
343
537
  version print package version
344
- init create .sns-myagent/config.json (defaults)
538
+ init create .sns-myagent/config.json + connect AI provider
539
+ setup alias for init (BYOK provider setup)
345
540
  chat [--stub] start interactive chat (stub for Phase 2B)
346
541
  launch alias for default
347
542
  config show print current config
348
543
  config get <key> read a dot-path value (e.g. model.provider)
349
544
  config set <key> <value> update a dot-path value
350
545
  telegram start|stop|status manage the Telegram polling adapter
351
- orchestrate <prompt> multi-agent ensemble run (Phase 5)
546
+ orchestrate <prompt> multi-agent ensemble run
352
547
  help print this help
353
548
  `;
354
549
 
@@ -370,6 +565,7 @@ export async function runCliAsync(argv: string[]): Promise<number> {
370
565
  case "-v":
371
566
  return cmdVersion();
372
567
  case "init":
568
+ case "setup":
373
569
  return cmdInit();
374
570
  case "chat":
375
571
  return cmdChat(rest);
@@ -377,8 +573,6 @@ export async function runCliAsync(argv: string[]): Promise<number> {
377
573
  return cmdConfig(rest);
378
574
  case "telegram":
379
575
  return cmdTelegram(rest);
380
- case "launch":
381
- return cmdLaunch(rest);
382
576
  case "orchestrate":
383
577
  return cmdOrchestrate(rest);
384
578
  default:
@@ -392,30 +586,55 @@ async function cmdOrchestrate(args: string[]): Promise<number> {
392
586
  if (args.length === 0) {
393
587
  process.stderr.write("✗ orchestrate requires a prompt\n");
394
588
  process.stderr.write(" usage: snsagent orchestrate <prompt>\n");
395
- process.stderr.write(" flags: --strategy consensus|critic|best_of_n\n");
589
+ process.stderr.write(" flags: --strategy consensus|critic|best_of_n|single\n");
396
590
  process.stderr.write(" --agents role1,role2\n");
397
591
  process.stderr.write(" --ensemble <name> (from agents.yaml)\n");
398
592
  return 1;
399
593
  }
400
- const prompt = args.join(" ");
401
- const opts: Record<string, unknown> = {};
594
+
595
+ // Parse flags and build prompt
596
+ const promptParts: string[] = [];
597
+ let strategy: string | undefined;
598
+ let agents: string[] | undefined;
599
+ let ensemble: string | undefined;
600
+
402
601
  for (let i = 0; i < args.length; i++) {
403
602
  const a = args[i];
404
- if (a === "--strategy") opts.strategy = args[++i];
405
- else if (a === "--agents") opts.agents = (args[++i] ?? "").split(",").filter(Boolean);
406
- else if (a === "--ensemble") opts.ensemble = args[++i];
603
+ if (a === "--strategy") { strategy = args[++i]; }
604
+ else if (a === "--agents") { agents = (args[++i] ?? "").split(",").filter(Boolean); }
605
+ else if (a === "--ensemble") { ensemble = args[++i]; }
606
+ else { promptParts.push(a); }
407
607
  }
408
- void prompt; void opts; // parsed for future executor wiring (Task 5.1d)
608
+
609
+ const prompt = promptParts.join(" ");
610
+ if (!prompt) {
611
+ process.stderr.write("✗ orchestrate requires a prompt\n");
612
+ return 1;
613
+ }
614
+
409
615
  try {
410
- // CLI wrapper: no real LLM executor yet (Phase 5 stub).
411
- // Until agent invocation is wired, surface a clear message instead of silently failing.
412
- process.stderr.write(
413
- "✗ orchestrate: agent executor not wired in CLI yet.\n" +
414
- " Phase 5 ships the orchestrator module; the CLI integration lands once\n" +
415
- " the LLM agent executor (src/agents/executor.ts) is implemented.\n" +
416
- " For now use the ensemble module directly: import { executeEnsemble } from \"../agents/ensemble.js\".\n",
417
- );
418
- return 2;
616
+ const { executeEnsemble } = await import("../agents/ensemble.js");
617
+ const { executeAgentForEnsemble } = await import("../agents/executor.js");
618
+
619
+ const result = await executeEnsemble(prompt, executeAgentForEnsemble, {
620
+ strategy: strategy as "consensus" | "critic" | "best_of_n" | undefined,
621
+ agents,
622
+ ensemble,
623
+ });
624
+
625
+ // Output result
626
+ console.log(result.final);
627
+
628
+ // Cost summary to stderr
629
+ if (result.costBreakdown.length > 0) {
630
+ process.stderr.write(`\n── ensemble: ${result.strategy} | ${result.rounds} round(s) | ${result.totalTimeMs}ms ──\n`);
631
+ for (const row of result.costBreakdown) {
632
+ const tokens = row.inputTokens + row.outputTokens;
633
+ process.stderr.write(` ${row.role}: ${row.model} (${tokens} tokens)\n`);
634
+ }
635
+ }
636
+
637
+ return 0;
419
638
  } catch (err) {
420
639
  process.stderr.write(`✗ orchestrate failed: ${(err as Error).message}\n`);
421
640
  return 1;