nolo-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # @nolo/cli
2
+
3
+ Agent-first terminal client for Nolo.
4
+
5
+ `nolo` should be understood as the terminal workspace for Nolo agents. The
6
+ existing command mode still wraps repo scripts, but the product direction is a
7
+ TUI-first experience similar to Claude Code / Codex CLI:
8
+
9
+ - open `nolo` and work inside a persistent Agent session;
10
+ - chat with an Agent;
11
+ - inspect and manage that Agent's dialogs, docs, tables, skills, and spaces;
12
+ - run internal ops/doctor agents with the same mental model;
13
+ - use non-interactive commands and `--json` for automation.
14
+
15
+ The current MVP uses a dependency-free readline workspace. It is intentionally
16
+ small: status line, text input, lightweight slash commands, and script-backed
17
+ Agent chat. A richer Ink UI can replace the rendering layer later without
18
+ changing the session model.
19
+
20
+ ## Usage
21
+
22
+ ```bash
23
+ npm install -g nolo-cli
24
+ nolo
25
+
26
+ # update later
27
+ npm update -g nolo-cli
28
+ ```
29
+
30
+ The npm package expects Bun to be available because the executable is a Bun
31
+ TypeScript script. For agent calls outside this repository, provide a token:
32
+
33
+ ```bash
34
+ NOLO_SERVER=https://nolo.chat AUTH_TOKEN=<token> nolo
35
+ ```
36
+
37
+ Local repo development can still use the script bridge without `AUTH_TOKEN`.
38
+
39
+ ```bash
40
+ nolo --help
41
+ nolo
42
+ nolo chat
43
+ nolo run "summarize my latest agent dialogs"
44
+ nolo doc create --title "Trip Notes" --body "hello"
45
+ nolo skill-doc create --title "Agent Query Skill" --description "Inspect recent agent dialogs"
46
+ nolo agent list --json
47
+ nolo chat --agent agent-pub-01APPBUILDER00000001YAII3I --msg "你好"
48
+ ```
49
+
50
+ ## Product Shape
51
+
52
+ The preferred command model is Agent-first:
53
+
54
+ ```bash
55
+ nolo agent list
56
+ nolo agent switch <agent>
57
+ nolo agent read <agent>
58
+ nolo agent run <agent> "检查最近失败任务"
59
+ nolo dialog list --agent <agent>
60
+ nolo dialog read <dialog>
61
+ nolo doc list --agent <agent>
62
+ nolo table query <table> --agent <agent>
63
+ ```
64
+
65
+ Inside the future TUI, the same actions should be available as slash commands:
66
+
67
+ ```text
68
+ /agent list
69
+ /agent switch ops
70
+ /dialog open latest
71
+ /doc list
72
+ /table query builtin-dialog-probe-runs
73
+ ```
74
+
75
+ See [`docs/nolo-cli-tui.md`](../../docs/nolo-cli-tui.md) for the product and
76
+ technical direction.
@@ -0,0 +1,139 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ type EnvLike = Record<string, string | undefined>;
5
+
6
+ type OutputLike = {
7
+ write(chunk: string): unknown;
8
+ };
9
+
10
+ type RunAgentTurnOptions = {
11
+ agentName: string;
12
+ agentKey: string;
13
+ serverUrl: string;
14
+ message: string;
15
+ scriptDir: string;
16
+ env: EnvLike;
17
+ output: OutputLike;
18
+ scriptPathExists?: (path: string) => boolean;
19
+ fetchImpl?: typeof fetch;
20
+ };
21
+
22
+ type ScriptBridgeDecision = {
23
+ hasAuthToken: boolean;
24
+ scriptPathExists: boolean;
25
+ };
26
+
27
+ export function shouldUseScriptBridge(decision: ScriptBridgeDecision) {
28
+ return !decision.hasAuthToken && decision.scriptPathExists;
29
+ }
30
+
31
+ function resolveAuthToken(env: EnvLike) {
32
+ return env.AUTH_TOKEN || env.AUTH || env.BENCHMARK_AUTH_TOKEN || "";
33
+ }
34
+
35
+ function formatUsage(usage: any, dialogId: unknown) {
36
+ const parts: string[] = [];
37
+ if (typeof dialogId === "string" && dialogId) parts.push(`dialog=${dialogId}`);
38
+
39
+ const input = usage?.input_tokens ?? usage?.prompt_tokens ?? 0;
40
+ const output = usage?.output_tokens ?? usage?.completion_tokens ?? 0;
41
+ if (input || output) parts.push(`tokens=${input}+${output}`);
42
+
43
+ return parts.length ? ` (${parts.join(" ")})` : "";
44
+ }
45
+
46
+ async function runScriptBridge(options: RunAgentTurnOptions, scriptPath: string) {
47
+ options.output.write(`\n${options.agentName} -> working via local script...\n\n`);
48
+ const proc = Bun.spawn({
49
+ cmd: [
50
+ process.execPath,
51
+ scriptPath,
52
+ "--agent",
53
+ options.agentKey,
54
+ "--server",
55
+ options.serverUrl,
56
+ "--msg",
57
+ options.message,
58
+ "--no-default-test-root",
59
+ ],
60
+ stdin: "inherit",
61
+ stdout: "inherit",
62
+ stderr: "inherit",
63
+ env: {
64
+ ...process.env,
65
+ ...options.env,
66
+ ASSISTANT_LABEL: options.agentName,
67
+ },
68
+ });
69
+
70
+ const exitCode = await proc.exited;
71
+ if (exitCode !== 0) {
72
+ options.output.write(`\n[nolo] Agent run exited with code ${exitCode}.\n`);
73
+ }
74
+ return exitCode;
75
+ }
76
+
77
+ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string) {
78
+ options.output.write(`\n${options.agentName} -> working...\n\n`);
79
+
80
+ const fetchImpl = options.fetchImpl ?? fetch;
81
+ const res = await fetchImpl(`${options.serverUrl}/api/agent/run`, {
82
+ method: "POST",
83
+ headers: {
84
+ Authorization: `Bearer ${authToken}`,
85
+ "Content-Type": "application/json",
86
+ },
87
+ body: JSON.stringify({
88
+ agentKey: options.agentKey,
89
+ userInput: options.message,
90
+ stream: false,
91
+ }),
92
+ });
93
+
94
+ let data: any = {};
95
+ try {
96
+ data = await res.json();
97
+ } catch {
98
+ data = {};
99
+ }
100
+
101
+ if (!res.ok) {
102
+ options.output.write(`[nolo] Agent request failed: HTTP ${res.status}\n`);
103
+ if (data?.error || data?.message) {
104
+ options.output.write(`${data.error || data.message}\n`);
105
+ }
106
+ return 1;
107
+ }
108
+
109
+ const content = String(data?.content ?? data?.message ?? "").trim();
110
+ if (content) {
111
+ options.output.write(`${options.agentName}: ${content}\n`);
112
+ } else {
113
+ options.output.write(`${options.agentName}: (no text response)\n`);
114
+ }
115
+
116
+ const usage = formatUsage(data?.usage, data?.dialogId);
117
+ if (usage) options.output.write(`${usage}\n`);
118
+ return 0;
119
+ }
120
+
121
+ export async function runAgentTurn(options: RunAgentTurnOptions) {
122
+ const authToken = resolveAuthToken(options.env);
123
+ const scriptPath = join(options.scriptDir, "chatWithAgent.ts");
124
+ const scriptPathExists = (options.scriptPathExists ?? existsSync)(scriptPath);
125
+
126
+ if (shouldUseScriptBridge({ hasAuthToken: Boolean(authToken), scriptPathExists })) {
127
+ return runScriptBridge(options, scriptPath);
128
+ }
129
+
130
+ if (!authToken) {
131
+ options.output.write(
132
+ "[nolo] This install needs an auth token before it can talk to agents.\n" +
133
+ "Set AUTH_TOKEN, or use the repo-local CLI where the dev script bridge is available.\n"
134
+ );
135
+ return 1;
136
+ }
137
+
138
+ return runHttpAgentTurn(options, authToken);
139
+ }
@@ -0,0 +1,95 @@
1
+ export type CommandEntry = {
2
+ path: string[];
3
+ script: string;
4
+ description: string;
5
+ };
6
+
7
+ export const COMMANDS: CommandEntry[] = [
8
+ { path: ["chat"], script: "chatWithAgent.ts", description: "Chat with an agent" },
9
+
10
+ { path: ["doc", "create"], script: "createDoc.ts", description: "Create a normal doc" },
11
+ { path: ["doc", "read"], script: "readDoc.ts", description: "Read a normal doc" },
12
+ { path: ["doc", "update"], script: "updateDoc.ts", description: "Update a normal doc" },
13
+ { path: ["doc", "delete"], script: "deleteDoc.ts", description: "Delete a normal doc" },
14
+
15
+ { path: ["skill-doc", "create"], script: "createSkillDoc.ts", description: "Create a skill doc" },
16
+ { path: ["skill-doc", "read"], script: "readSkillDoc.ts", description: "Read a skill doc" },
17
+ { path: ["skill-doc", "update"], script: "updateSkillDoc.ts", description: "Update a skill doc" },
18
+ { path: ["skill-doc", "delete"], script: "deleteSkillDoc.ts", description: "Delete a skill doc" },
19
+
20
+ { path: ["dialog", "read"], script: "readDialog.ts", description: "Read a dialog" },
21
+ { path: ["space", "read"], script: "readSpace.ts", description: "Read a space" },
22
+ { path: ["space", "category"], script: "upsertSpaceCategory.ts", description: "Create or update a space category" },
23
+ { path: ["space", "content-category"], script: "setSpaceContentCategory.ts", description: "Move content into a space category" },
24
+
25
+ { path: ["table", "data"], script: "tableData.ts", description: "Query or mutate table rows" },
26
+ { path: ["table", "meta"], script: "upsertTableMeta.ts", description: "Create or update table metadata" },
27
+
28
+ { path: ["agent", "list"], script: "listMyAgents.ts", description: "List owned agents" },
29
+ { path: ["agent", "read"], script: "readAgent.ts", description: "Read a single agent" },
30
+ { path: ["agent", "update"], script: "updateAgent.ts", description: "Update agent fields" },
31
+ { path: ["agent", "unpublish"], script: "unpublishAgent.ts", description: "Remove an agent's public record" },
32
+ { path: ["agent", "chat"], script: "chatWithAgent.ts", description: "Chat with an agent" },
33
+ { path: ["agent", "dialogs"], script: "queryAgentDialogs.ts", description: "Inspect recent dialogs for an agent" },
34
+ { path: ["agent", "doctor"], script: "doctorAgentWorkspace.ts", description: "Audit agent workspace health" },
35
+ { path: ["agent", "create-custom"], script: "createCustomCodingAgent.ts", description: "Create a custom coding agent" },
36
+ { path: ["agent", "create-space"], script: "createSpaceAgents.ts", description: "Create or attach shared-space agents" },
37
+ { path: ["agent", "setup-demo"], script: "setupDemoAgent.ts", description: "Bootstrap demo publisher agents" },
38
+ { path: ["agent", "supervise"], script: "runAutonomousAgent.ts", description: "Run an agent in autonomous cycles" },
39
+
40
+ { path: ["llama"], script: "llamaServerSupervisor.ts", description: "Manage the llama.cpp runtime" },
41
+ { path: ["model-runtime"], script: "localModelRuntimeSupervisor.ts", description: "Manage local model runtime processes" },
42
+ { path: ["dev"], script: "devControl.ts", description: "Manage the local dev environment" },
43
+ ];
44
+
45
+ export const GROUP_ORDER = [
46
+ "chat",
47
+ "doc",
48
+ "skill-doc",
49
+ "agent",
50
+ "dialog",
51
+ "space",
52
+ "table",
53
+ "llama",
54
+ "model-runtime",
55
+ "dev",
56
+ ] as const;
57
+
58
+ export function renderHelpText() {
59
+ const lines = [
60
+ "nolo — Agent-first terminal workspace",
61
+ "",
62
+ "Usage:",
63
+ " nolo",
64
+ " nolo <command> [subcommand] [...args]",
65
+ "",
66
+ "Examples:",
67
+ " nolo",
68
+ " nolo chat",
69
+ ' nolo doc create --title "Trip Notes" --body "hello"',
70
+ ' nolo skill-doc create --title "Agent Query Skill" --description "Inspect recent agent dialogs"',
71
+ " nolo agent list --json",
72
+ " nolo agent read agent-pub-01APPBUILDER00000001YAII3I",
73
+ ' nolo chat --agent agent-pub-01APPBUILDER00000001YAII3I --msg "你好"',
74
+ " nolo table data --table 01ABCXYZ --action query",
75
+ " nolo llama status",
76
+ ];
77
+
78
+ for (const group of GROUP_ORDER) {
79
+ const commands = COMMANDS.filter((entry) => entry.path[0] === group);
80
+ if (commands.length === 0) continue;
81
+ lines.push("", group);
82
+ for (const entry of commands) {
83
+ lines.push(` ${entry.path.join(" ")} ${entry.description}`);
84
+ }
85
+ }
86
+
87
+ return lines.join("\n");
88
+ }
89
+
90
+ export function resolveCommand(args: string[]) {
91
+ const sorted = [...COMMANDS].sort((a, b) => b.path.length - a.path.length);
92
+ return sorted.find((entry) =>
93
+ entry.path.every((part, index) => args[index] === part)
94
+ );
95
+ }
package/index.ts ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import { renderHelpText, resolveCommand } from "./commandRegistry";
7
+ import { startTuiWorkspace } from "./tui/readlineWorkspace";
8
+
9
+ const CLI_DIR = dirname(fileURLToPath(import.meta.url));
10
+ const ROOT_DIR = join(CLI_DIR, "..", "..");
11
+ const SCRIPT_DIR = join(ROOT_DIR, "scripts");
12
+
13
+ async function runScript(script: string, forwardedArgs: string[]) {
14
+ const scriptPath = join(SCRIPT_DIR, script);
15
+ const proc = Bun.spawn({
16
+ cmd: [process.execPath, scriptPath, ...forwardedArgs],
17
+ stdin: "inherit",
18
+ stdout: "inherit",
19
+ stderr: "inherit",
20
+ env: process.env,
21
+ });
22
+ const exitCode = await proc.exited;
23
+ process.exit(exitCode);
24
+ }
25
+
26
+ const args = process.argv.slice(2);
27
+
28
+ if (args.length === 0) {
29
+ if (process.stdin.isTTY) {
30
+ await startTuiWorkspace({ scriptDir: SCRIPT_DIR });
31
+ } else {
32
+ console.log(renderHelpText());
33
+ }
34
+ process.exit(0);
35
+ }
36
+
37
+ if (args.length === 1 && (args[0] === "tui" || args[0] === "chat")) {
38
+ await startTuiWorkspace({ scriptDir: SCRIPT_DIR });
39
+ process.exit(0);
40
+ }
41
+
42
+ if (args[0] === "--help" || args[0] === "-h") {
43
+ console.log(renderHelpText());
44
+ process.exit(0);
45
+ }
46
+
47
+ const command = resolveCommand(args);
48
+ if (!command) {
49
+ console.error(`Unknown command: ${args.join(" ")}`);
50
+ console.error("");
51
+ console.log(renderHelpText());
52
+ process.exit(1);
53
+ }
54
+
55
+ const forwardedArgs = args.slice(command.path.length);
56
+ await runScript(command.script, forwardedArgs);
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "nolo-cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Agent-first terminal workspace for Nolo",
6
+ "bin": {
7
+ "nolo": "./index.ts"
8
+ },
9
+ "module": "index.ts",
10
+ "files": [
11
+ "index.ts",
12
+ "commandRegistry.ts",
13
+ "client/agentRun.ts",
14
+ "tui/readlineWorkspace.ts",
15
+ "tui/session.ts",
16
+ "README.md"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "devDependencies": {
22
+ "bun-types": "latest"
23
+ },
24
+ "peerDependencies": {
25
+ "typescript": "^5.0.0"
26
+ }
27
+ }
@@ -0,0 +1,79 @@
1
+ import { createInterface } from "node:readline";
2
+ import { stdin as defaultInput, stdout as defaultOutput } from "node:process";
3
+ import { runAgentTurn } from "../client/agentRun";
4
+ import {
5
+ createInitialTuiState,
6
+ handleTuiInput,
7
+ renderPrompt,
8
+ renderStatusLine,
9
+ renderWelcome,
10
+ type TuiState,
11
+ } from "./session";
12
+
13
+ type WorkspaceOptions = {
14
+ scriptDir: string;
15
+ env?: NodeJS.ProcessEnv;
16
+ input?: NodeJS.ReadableStream;
17
+ output?: NodeJS.WritableStream;
18
+ };
19
+
20
+ async function runAgentChat(
21
+ scriptDir: string,
22
+ state: TuiState,
23
+ message: string,
24
+ env: NodeJS.ProcessEnv,
25
+ output: NodeJS.WritableStream
26
+ ) {
27
+ const exitCode = await runAgentTurn({
28
+ agentName: state.agentName,
29
+ agentKey: state.agentKey,
30
+ serverUrl: state.serverUrl,
31
+ message,
32
+ scriptDir,
33
+ env,
34
+ output,
35
+ });
36
+ return exitCode;
37
+ }
38
+
39
+ export async function startTuiWorkspace(options: WorkspaceOptions) {
40
+ let state = createInitialTuiState(options.env ?? process.env);
41
+ const input = options.input ?? defaultInput;
42
+ const output = options.output ?? defaultOutput;
43
+ const rl = createInterface({ input, output });
44
+
45
+ output.write(renderWelcome(state));
46
+ rl.setPrompt(renderPrompt(state));
47
+ rl.prompt();
48
+
49
+ try {
50
+ for await (const line of rl) {
51
+ const result = handleTuiInput(line, state);
52
+ state = result.nextState;
53
+
54
+ if (result.output) {
55
+ output.write(`${result.output}\n`);
56
+ }
57
+
58
+ if (result.action?.type === "exit") {
59
+ break;
60
+ }
61
+
62
+ if (result.action?.type === "chat") {
63
+ await runAgentChat(
64
+ options.scriptDir,
65
+ state,
66
+ result.action.message,
67
+ options.env ?? process.env,
68
+ output
69
+ );
70
+ }
71
+
72
+ output.write(`\n${renderStatusLine(state)}\n`);
73
+ rl.setPrompt(renderPrompt(state));
74
+ rl.prompt();
75
+ }
76
+ } finally {
77
+ rl.close();
78
+ }
79
+ }
package/tui/session.ts ADDED
@@ -0,0 +1,232 @@
1
+ export const DEFAULT_TUI_AGENT_KEY = "agent-pub-01NOLOAPPBLD000000019KCKT0";
2
+ export const DEFAULT_TUI_SERVER_URL = "http://127.0.0.1:38123";
3
+
4
+ const KNOWN_AGENT_ALIASES: Record<string, { name: string; key: string }> = {
5
+ nolo: {
6
+ name: "nolo",
7
+ key: DEFAULT_TUI_AGENT_KEY,
8
+ },
9
+ "app-builder": {
10
+ name: "app-builder",
11
+ key: "agent-pub-01APPBUILDER00000001YAII3I",
12
+ },
13
+ };
14
+
15
+ export type TuiState = {
16
+ agentKey: string;
17
+ agentName: string;
18
+ dialogLabel: string;
19
+ profileName: string;
20
+ serverUrl: string;
21
+ attachedDocs: string[];
22
+ };
23
+
24
+ export type TuiAction =
25
+ | {
26
+ type: "chat";
27
+ message: string;
28
+ agentKey: string;
29
+ }
30
+ | {
31
+ type: "exit";
32
+ };
33
+
34
+ export type TuiInputResult = {
35
+ nextState: TuiState;
36
+ output: string;
37
+ action?: TuiAction;
38
+ };
39
+
40
+ type EnvLike = Record<string, string | undefined>;
41
+
42
+ export function createInitialTuiState(env: EnvLike = process.env): TuiState {
43
+ const agentKey = env.NOLO_AGENT?.trim() || DEFAULT_TUI_AGENT_KEY;
44
+ const agentName = env.NOLO_AGENT_NAME?.trim() || "nolo";
45
+
46
+ return {
47
+ agentKey,
48
+ agentName,
49
+ dialogLabel: env.NOLO_DIALOG?.trim() || "new",
50
+ profileName: env.NOLO_PROFILE?.trim() || "local",
51
+ serverUrl: (env.NOLO_SERVER || env.BASE_URL || DEFAULT_TUI_SERVER_URL).replace(/\/+$/, ""),
52
+ attachedDocs: [],
53
+ };
54
+ }
55
+
56
+ export function renderStatusLine(state: TuiState) {
57
+ const docs =
58
+ state.attachedDocs.length > 0
59
+ ? state.attachedDocs.join(", ")
60
+ : "0 attached";
61
+
62
+ return [
63
+ `agent ${state.agentName}`,
64
+ `dialog ${state.dialogLabel}`,
65
+ `docs ${docs}`,
66
+ `profile ${state.profileName}`,
67
+ ].join(" | ");
68
+ }
69
+
70
+ export function renderWelcome(state: TuiState) {
71
+ return [
72
+ "",
73
+ "Nolo workspace",
74
+ "--------------",
75
+ `agent ${state.agentName}`,
76
+ `dialog ${state.dialogLabel}`,
77
+ `docs ${state.attachedDocs.length ? state.attachedDocs.join(", ") : "none"}`,
78
+ `profile ${state.profileName}`,
79
+ `server ${state.serverUrl}`,
80
+ "",
81
+ "Tell nolo what you want. Use /help for commands.",
82
+ "",
83
+ ].join("\n");
84
+ }
85
+
86
+ export function renderPrompt(_state: TuiState) {
87
+ return "you > ";
88
+ }
89
+
90
+ export function renderTuiHelp() {
91
+ return [
92
+ "Commands:",
93
+ " /help Show this help",
94
+ " /new Start a fresh dialog label",
95
+ " /agent Show the current agent",
96
+ " /switch <agent> Switch the current agent",
97
+ " /dialog Show the current dialog",
98
+ " /doc List attached docs",
99
+ " /doc attach <doc> Attach a doc to this workspace",
100
+ " /customize Describe how you want to tune nolo",
101
+ " /login Show login/profile hint",
102
+ " /profile Show active profile",
103
+ " /exit Leave the workspace",
104
+ "",
105
+ "You can also type normally. nolo will send it to the current agent.",
106
+ ].join("\n");
107
+ }
108
+
109
+ function resolveSwitchTarget(rawTarget: string) {
110
+ const target = rawTarget.trim();
111
+ const alias = KNOWN_AGENT_ALIASES[target.toLowerCase()];
112
+ if (alias) return alias;
113
+ if (/^\d+$/.test(target)) return null;
114
+ if (target.startsWith("agent-") || target.startsWith("agent-pub-")) {
115
+ return { name: target, key: target };
116
+ }
117
+ return null;
118
+ }
119
+
120
+ export function handleTuiInput(input: string, state: TuiState): TuiInputResult {
121
+ const trimmed = input.trim();
122
+ if (!trimmed) {
123
+ return { nextState: state, output: "" };
124
+ }
125
+
126
+ if (!trimmed.startsWith("/")) {
127
+ return {
128
+ nextState: state,
129
+ output: "",
130
+ action: {
131
+ type: "chat",
132
+ message: trimmed,
133
+ agentKey: state.agentKey,
134
+ },
135
+ };
136
+ }
137
+
138
+ const [command = "", ...rest] = trimmed.split(/\s+/);
139
+ const argText = rest.join(" ").trim();
140
+
141
+ switch (command) {
142
+ case "/help":
143
+ return { nextState: state, output: renderTuiHelp() };
144
+ case "/exit":
145
+ case "/quit":
146
+ return { nextState: state, output: "Bye.", action: { type: "exit" } };
147
+ case "/new":
148
+ return {
149
+ nextState: { ...state, dialogLabel: "new", attachedDocs: [] },
150
+ output: "Started a fresh workspace.",
151
+ };
152
+ case "/agent":
153
+ return {
154
+ nextState: state,
155
+ output: `Current agent: ${state.agentName} (${state.agentKey})`,
156
+ };
157
+ case "/switch": {
158
+ if (!argText) {
159
+ return {
160
+ nextState: state,
161
+ output: "Usage: /switch <agent-key|alias>",
162
+ };
163
+ }
164
+ const resolvedTarget = resolveSwitchTarget(argText);
165
+ if (!resolvedTarget) {
166
+ return {
167
+ nextState: state,
168
+ output:
169
+ `I don't know agent shortcut "${argText}" yet.\n` +
170
+ "Use /switch nolo, /switch app-builder, or a full agent key like agent-pub-...",
171
+ };
172
+ }
173
+ return {
174
+ nextState: {
175
+ ...state,
176
+ agentName: resolvedTarget.name,
177
+ agentKey: resolvedTarget.key,
178
+ },
179
+ output: `Switched to ${resolvedTarget.name}.`,
180
+ };
181
+ }
182
+ case "/dialog":
183
+ return {
184
+ nextState: state,
185
+ output: `Current dialog: ${state.dialogLabel}`,
186
+ };
187
+ case "/doc": {
188
+ if (rest[0] === "attach") {
189
+ const docName = rest.slice(1).join(" ").trim();
190
+ if (!docName) {
191
+ return { nextState: state, output: "Usage: /doc attach <doc>" };
192
+ }
193
+ const attachedDocs = state.attachedDocs.includes(docName)
194
+ ? state.attachedDocs
195
+ : [...state.attachedDocs, docName];
196
+ return {
197
+ nextState: { ...state, attachedDocs },
198
+ output: `Attached doc: ${docName}`,
199
+ };
200
+ }
201
+ return {
202
+ nextState: state,
203
+ output:
204
+ state.attachedDocs.length > 0
205
+ ? `Attached docs: ${state.attachedDocs.join(", ")}`
206
+ : "No docs attached. Use /doc attach <doc>.",
207
+ };
208
+ }
209
+ case "/customize":
210
+ return {
211
+ nextState: state,
212
+ output:
213
+ "Tell nolo what to change, for example: /customize make my default agent more concise.",
214
+ };
215
+ case "/login":
216
+ return {
217
+ nextState: state,
218
+ output:
219
+ "MVP login uses profile/env auth. Set AUTH_TOKEN, NOLO_SERVER, or NOLO_PROFILE before starting nolo.",
220
+ };
221
+ case "/profile":
222
+ return {
223
+ nextState: state,
224
+ output: `Profile: ${state.profileName}`,
225
+ };
226
+ default:
227
+ return {
228
+ nextState: state,
229
+ output: `Unknown command: ${command}\n\n${renderTuiHelp()}`,
230
+ };
231
+ }
232
+ }