@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.
- package/CHANGELOG.md +8 -0
- package/README.md +63 -24
- package/bin/snsagent.js +3 -41
- package/dist/cli.js +3267 -3288
- package/package.json +3 -3
- package/src/adapters/telegram/handler.ts +1 -1
- package/src/agents/ensemble.ts +3 -4
- package/src/agents/executor.ts +129 -0
- package/src/async/notifier.ts +1 -1
- package/src/cli/config-cli.ts +1 -1
- package/src/cli/entry.ts +2 -2
- package/src/cli/gallery-fixtures/misc.ts +3 -3
- package/src/cli/grep-cli.ts +3 -3
- package/src/cli/grievances-cli.ts +2 -2
- package/src/cli/index.ts +243 -24
- package/src/cli/plugin-cli.ts +5 -5
- package/src/cli/setup-cli.ts +3 -3
- package/src/cli/setup-model-picker.ts +1 -1
- package/src/cli/shell-cli.ts +1 -1
- package/src/cli/ssh-cli.ts +1 -1
- package/src/cli/stats-cli.ts +1 -1
- package/src/cli/tiny-models-cli.ts +3 -3
- package/src/cli/ttsr-cli.ts +2 -2
- package/src/cli/update-cli.ts +3 -3
- package/src/cli/usage-cli.ts +1 -1
- package/src/cli-commands.ts +6 -6
- package/src/commands/acp.ts +2 -2
- package/src/commands/chat.ts +3 -3
- package/src/commands/install.ts +5 -5
- package/src/commands/say.ts +4 -4
- package/src/commands/setup.ts +1 -1
- package/src/config/defaults.ts +5 -3
- package/src/config/schema.ts +8 -0
- package/src/config/settings-schema.ts +1 -1
- package/src/config/sns-config.ts +1 -1
- package/src/extensibility/plugins/loader.ts +1 -1
- package/src/extensibility/plugins/manager.ts +2 -2
- package/src/hindsight/client.ts +1 -1
- package/src/internal-urls/docs-index.generated.txt +2 -0
- package/src/main.ts +4 -0
- package/src/modes/acp/acp-agent.ts +3 -3
- package/src/modes/components/plugin-settings.ts +2 -2
- package/src/modes/components/welcome.ts +66 -160
- package/src/modes/interactive-mode.ts +2 -2
- package/src/modes/print-mode.ts +2 -2
- package/src/modes/setup-version.ts +1 -1
- package/src/modes/setup-wizard/scenes/byok-setup.ts +354 -0
- package/src/modes/setup-wizard/scenes/outro.ts +2 -2
- package/src/modes/setup-wizard/scenes/providers.ts +6 -5
- package/src/modes/setup-wizard/scenes/splash.ts +3 -3
- package/src/modes/setup-wizard/wizard-overlay.ts +2 -2
- package/src/modes/theme/dark.json +21 -20
- package/src/modes/theme/light.json +30 -29
- package/src/modes/utils/ui-helpers.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +6 -3
- package/src/stt/recorder.ts +3 -3
- package/src/tts/player.ts +1 -1
- package/src/tts/tts-client.ts +1 -1
- package/src/tui/chat-blocks.ts +4 -4
- package/src/tui/chat-ui.ts +3 -3
- package/src/tui/code-cell.ts +3 -3
- package/src/tui/command-palette.ts +3 -3
- package/src/tui/splash.ts +12 -18
- package/src/ui/banner.ts +51 -66
- package/src/ui/chat-prompt.ts +2 -2
- package/src/ui/colors.ts +17 -14
- package/src/ui/error-display.ts +2 -2
- package/src/ui/gradient.ts +3 -3
- package/src/ui/memory-toast.ts +5 -5
- 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.
|
|
5
|
-
"description": "SNS-MyAgent
|
|
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.
|
|
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
|
|
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.",
|
package/src/agents/ensemble.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/async/notifier.ts
CHANGED
|
@@ -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.
|
|
13
|
+
running: (t) => chalk.hex("#F97316")(t),
|
|
14
14
|
pending: (t) => chalk.dim(t),
|
|
15
15
|
};
|
|
16
16
|
|
package/src/cli/config-cli.ts
CHANGED
|
@@ -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.
|
|
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/
|
|
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
|
|
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
|
-
"●
|
|
123
|
-
" Loaded: loaded (/etc/systemd/system/
|
|
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/
|
|
129
|
+
" CGroup: /system.slice/snsagent-api.service",
|
|
130
130
|
" └─4812 /usr/local/bin/bun run dist/server.js",
|
|
131
131
|
].join("\n"),
|
|
132
132
|
},
|
package/src/cli/grep-cli.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
122
|
+
console.log(`${chalk.hex("#F97316")(displayPath)}: ${match.matchCount ?? 0} matches`);
|
|
123
123
|
} else {
|
|
124
|
-
console.log(chalk.
|
|
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.
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
401
|
-
|
|
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")
|
|
405
|
-
else if (a === "--agents")
|
|
406
|
-
else if (a === "--ensemble")
|
|
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
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
);
|
|
418
|
-
|
|
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;
|