nolo-cli 0.1.2 → 0.1.4

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 CHANGED
@@ -17,14 +17,22 @@ small: status line, text input, lightweight slash commands, and script-backed
17
17
  Agent chat. A richer Ink UI can replace the rendering layer later without
18
18
  changing the session model.
19
19
 
20
+ Inside `nolo`, normal text continues the current dialog after the first agent
21
+ reply. Use `/new` when you want a clean dialog. Response token details stay
22
+ hidden by default; set `NOLO_SHOW_USAGE=1` when debugging usage.
23
+
20
24
  ## Usage
21
25
 
22
26
  ```bash
23
27
  npm install -g nolo-cli
24
28
  nolo
25
29
 
26
- # update later
27
- npm update -g nolo-cli
30
+ # see which install you are running
31
+ nolo doctor
32
+ nolo --version
33
+
34
+ # update later, from inside nolo itself
35
+ nolo update
28
36
  ```
29
37
 
30
38
  The npm package expects Bun to be available because the executable is a Bun
@@ -46,6 +54,8 @@ Local repo development can still use the script bridge without `AUTH_TOKEN`.
46
54
 
47
55
  ```bash
48
56
  nolo --help
57
+ nolo doctor
58
+ nolo update
49
59
  nolo
50
60
  nolo chat
51
61
  nolo run "summarize my latest agent dialogs"
@@ -12,6 +12,7 @@ type RunAgentTurnOptions = {
12
12
  agentKey: string;
13
13
  serverUrl: string;
14
14
  message: string;
15
+ continueDialogId?: string;
15
16
  scriptDir: string;
16
17
  env: EnvLike;
17
18
  output: OutputLike;
@@ -19,6 +20,11 @@ type RunAgentTurnOptions = {
19
20
  fetchImpl?: typeof fetch;
20
21
  };
21
22
 
23
+ export type RunAgentTurnResult = {
24
+ exitCode: number;
25
+ dialogId?: string;
26
+ };
27
+
22
28
  type ScriptBridgeDecision = {
23
29
  hasAuthToken: boolean;
24
30
  scriptPathExists: boolean;
@@ -32,6 +38,10 @@ function resolveAuthToken(env: EnvLike) {
32
38
  return env.AUTH_TOKEN || env.AUTH || env.BENCHMARK_AUTH_TOKEN || "";
33
39
  }
34
40
 
41
+ function shouldShowUsage(env: EnvLike) {
42
+ return env.NOLO_DEBUG === "1" || env.NOLO_SHOW_USAGE === "1";
43
+ }
44
+
35
45
  function formatUsage(usage: any, dialogId: unknown) {
36
46
  const parts: string[] = [];
37
47
  if (typeof dialogId === "string" && dialogId) parts.push(`dialog=${dialogId}`);
@@ -56,6 +66,9 @@ async function runScriptBridge(options: RunAgentTurnOptions, scriptPath: string)
56
66
  "--msg",
57
67
  options.message,
58
68
  "--no-default-test-root",
69
+ ...(options.continueDialogId
70
+ ? ["--continue", options.continueDialogId]
71
+ : []),
59
72
  ],
60
73
  stdin: "inherit",
61
74
  stdout: "inherit",
@@ -71,11 +84,11 @@ async function runScriptBridge(options: RunAgentTurnOptions, scriptPath: string)
71
84
  if (exitCode !== 0) {
72
85
  options.output.write(`\n[nolo] Agent run exited with code ${exitCode}.\n`);
73
86
  }
74
- return exitCode;
87
+ return { exitCode };
75
88
  }
76
89
 
77
90
  async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string) {
78
- options.output.write(`\n${options.agentName} -> working...\n\n`);
91
+ options.output.write(`\n${options.agentName} -> working...\n`);
79
92
 
80
93
  const fetchImpl = options.fetchImpl ?? fetch;
81
94
  const res = await fetchImpl(`${options.serverUrl}/api/agent/run`, {
@@ -87,6 +100,16 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
87
100
  body: JSON.stringify({
88
101
  agentKey: options.agentKey,
89
102
  userInput: options.message,
103
+ runtimeContext: {
104
+ surface: "cli",
105
+ host: "terminal",
106
+ runtime: "bun",
107
+ entrypoint: "nolo-cli",
108
+ capabilities: ["text-io", "slash-commands"],
109
+ },
110
+ ...(options.continueDialogId
111
+ ? { continueDialogId: options.continueDialogId }
112
+ : {}),
90
113
  stream: false,
91
114
  }),
92
115
  });
@@ -103,19 +126,24 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
103
126
  if (data?.error || data?.message) {
104
127
  options.output.write(`${data.error || data.message}\n`);
105
128
  }
106
- return 1;
129
+ return { exitCode: 1 };
107
130
  }
108
131
 
109
132
  const content = String(data?.content ?? data?.message ?? "").trim();
110
133
  if (content) {
111
- options.output.write(`${options.agentName}: ${content}\n`);
134
+ options.output.write(`\n${options.agentName}\n${content}\n`);
112
135
  } else {
113
- options.output.write(`${options.agentName}: (no text response)\n`);
136
+ options.output.write(`\n${options.agentName}\n(no text response)\n`);
114
137
  }
115
138
 
116
139
  const usage = formatUsage(data?.usage, data?.dialogId);
117
- if (usage) options.output.write(`${usage}\n`);
118
- return 0;
140
+ if (usage && shouldShowUsage(options.env)) options.output.write(`${usage}\n`);
141
+ return {
142
+ exitCode: 0,
143
+ ...(typeof data?.dialogId === "string" && data.dialogId
144
+ ? { dialogId: data.dialogId }
145
+ : {}),
146
+ };
119
147
  }
120
148
 
121
149
  export async function runAgentTurn(options: RunAgentTurnOptions) {
@@ -132,7 +160,7 @@ export async function runAgentTurn(options: RunAgentTurnOptions) {
132
160
  "[nolo] This install needs an auth token before it can talk to agents.\n" +
133
161
  "Set AUTH_TOKEN, or use the repo-local CLI where the dev script bridge is available.\n"
134
162
  );
135
- return 1;
163
+ return { exitCode: 1 };
136
164
  }
137
165
 
138
166
  return runHttpAgentTurn(options, authToken);
@@ -1,97 +1,101 @@
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() {
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
59
  const lines = [
60
60
  "nolo — Agent-first terminal workspace",
61
61
  "",
62
62
  "Usage:",
63
63
  " nolo",
64
64
  " nolo <command> [subcommand] [...args]",
65
+ " nolo doctor",
66
+ " nolo update",
65
67
  "",
66
68
  "Examples:",
67
69
  " nolo",
68
70
  " nolo chat",
69
71
  " nolo login",
70
72
  " nolo whoami",
73
+ " nolo doctor",
74
+ " nolo update",
71
75
  ' nolo doc create --title "Trip Notes" --body "hello"',
72
76
  ' nolo skill-doc create --title "Agent Query Skill" --description "Inspect recent agent dialogs"',
73
77
  " nolo agent list --json",
74
- " nolo agent read agent-pub-01APPBUILDER00000001YAII3I",
75
- ' nolo chat --agent agent-pub-01APPBUILDER00000001YAII3I --msg "你好"',
76
- " nolo table data --table 01ABCXYZ --action query",
77
- " nolo llama status",
78
- ];
79
-
80
- for (const group of GROUP_ORDER) {
81
- const commands = COMMANDS.filter((entry) => entry.path[0] === group);
82
- if (commands.length === 0) continue;
83
- lines.push("", group);
84
- for (const entry of commands) {
85
- lines.push(` ${entry.path.join(" ")} ${entry.description}`);
86
- }
87
- }
88
-
89
- return lines.join("\n");
90
- }
91
-
92
- export function resolveCommand(args: string[]) {
93
- const sorted = [...COMMANDS].sort((a, b) => b.path.length - a.path.length);
94
- return sorted.find((entry) =>
95
- entry.path.every((part, index) => args[index] === part)
96
- );
97
- }
78
+ " nolo agent read agent-pub-01APPBUILDER00000001YAII3I",
79
+ ' nolo chat --agent agent-pub-01APPBUILDER00000001YAII3I --msg "你好"',
80
+ " nolo table data --table 01ABCXYZ --action query",
81
+ " nolo llama status",
82
+ ];
83
+
84
+ for (const group of GROUP_ORDER) {
85
+ const commands = COMMANDS.filter((entry) => entry.path[0] === group);
86
+ if (commands.length === 0) continue;
87
+ lines.push("", group);
88
+ for (const entry of commands) {
89
+ lines.push(` ${entry.path.join(" ")} ${entry.description}`);
90
+ }
91
+ }
92
+
93
+ return lines.join("\n");
94
+ }
95
+
96
+ export function resolveCommand(args: string[]) {
97
+ const sorted = [...COMMANDS].sort((a, b) => b.path.length - a.path.length);
98
+ return sorted.find((entry) =>
99
+ entry.path.every((part, index) => args[index] === part)
100
+ );
101
+ }
package/index.ts CHANGED
@@ -1,35 +1,43 @@
1
1
  #!/usr/bin/env bun
2
-
3
- import { dirname, join } from "node:path";
2
+
3
+ import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
6
  import { renderHelpText, resolveCommand } from "./commandRegistry";
7
7
  import { runLoginCommand, runLogoutCommand, runWhoamiCommand } from "./authCommands";
8
8
  import { buildEnvFromProfile, loadProfileConfig } from "./client/profileConfig";
9
9
  import { startTuiWorkspace } from "./tui/readlineWorkspace";
10
-
11
- const CLI_DIR = dirname(fileURLToPath(import.meta.url));
12
- const ROOT_DIR = join(CLI_DIR, "..", "..");
13
- const SCRIPT_DIR = join(ROOT_DIR, "scripts");
14
-
15
- async function runScript(script: string, forwardedArgs: string[]) {
16
- const scriptPath = join(SCRIPT_DIR, script);
17
- const proc = Bun.spawn({
18
- cmd: [process.execPath, scriptPath, ...forwardedArgs],
19
- stdin: "inherit",
20
- stdout: "inherit",
21
- stderr: "inherit",
22
- env: process.env,
23
- });
24
- const exitCode = await proc.exited;
25
- process.exit(exitCode);
26
- }
27
-
10
+ import {
11
+ buildCliDoctorText,
12
+ buildCliVersionText,
13
+ readPackageInfo,
14
+ runSelfUpdate,
15
+ } from "./updateCommands";
16
+
17
+ const CLI_DIR = dirname(fileURLToPath(import.meta.url));
18
+ const ROOT_DIR = join(CLI_DIR, "..", "..");
19
+ const SCRIPT_DIR = join(ROOT_DIR, "scripts");
20
+ const packageInfo = readPackageInfo();
21
+
22
+ async function runScript(script: string, forwardedArgs: string[]) {
23
+ const scriptPath = join(SCRIPT_DIR, script);
24
+ const proc = Bun.spawn({
25
+ cmd: [process.execPath, scriptPath, ...forwardedArgs],
26
+ stdin: "inherit",
27
+ stdout: "inherit",
28
+ stderr: "inherit",
29
+ env: process.env,
30
+ });
31
+ const exitCode = await proc.exited;
32
+ process.exit(exitCode);
33
+ }
34
+
28
35
  const args = process.argv.slice(2);
29
36
  const profileEnv = buildEnvFromProfile(loadProfileConfig());
30
37
  const runtimeEnv = {
31
38
  ...profileEnv,
32
39
  ...process.env,
40
+ NOLO_CLI_VERSION: packageInfo.version,
33
41
  };
34
42
 
35
43
  if (args.length === 0) {
@@ -58,18 +66,40 @@ if (args[0] === "logout") {
58
66
  process.exit(runLogoutCommand());
59
67
  }
60
68
 
69
+ if (args[0] === "--version" || args[0] === "-v" || args[0] === "version") {
70
+ console.log(buildCliVersionText(packageInfo));
71
+ process.exit(0);
72
+ }
73
+
74
+ if (args[0] === "doctor") {
75
+ console.log(
76
+ buildCliDoctorText({
77
+ packageName: packageInfo.name,
78
+ version: packageInfo.version,
79
+ entrypoint: fileURLToPath(import.meta.url),
80
+ serverUrl: runtimeEnv.NOLO_SERVER || runtimeEnv.BASE_URL || "http://127.0.0.1:38123",
81
+ profileName: runtimeEnv.NOLO_PROFILE || "local",
82
+ })
83
+ );
84
+ process.exit(0);
85
+ }
86
+
87
+ if (args[0] === "update") {
88
+ process.exit(await runSelfUpdate());
89
+ }
90
+
61
91
  if (args[0] === "--help" || args[0] === "-h") {
62
92
  console.log(renderHelpText());
63
93
  process.exit(0);
64
94
  }
65
-
66
- const command = resolveCommand(args);
67
- if (!command) {
68
- console.error(`Unknown command: ${args.join(" ")}`);
69
- console.error("");
70
- console.log(renderHelpText());
71
- process.exit(1);
72
- }
73
-
74
- const forwardedArgs = args.slice(command.path.length);
75
- await runScript(command.script, forwardedArgs);
95
+
96
+ const command = resolveCommand(args);
97
+ if (!command) {
98
+ console.error(`Unknown command: ${args.join(" ")}`);
99
+ console.error("");
100
+ console.log(renderHelpText());
101
+ process.exit(1);
102
+ }
103
+
104
+ const forwardedArgs = args.slice(command.path.length);
105
+ await runScript(command.script, forwardedArgs);
package/package.json CHANGED
@@ -1,29 +1,30 @@
1
- {
1
+ {
2
2
  "name": "nolo-cli",
3
- "version": "0.1.2",
4
- "type": "module",
3
+ "version": "0.1.4",
4
+ "type": "module",
5
5
  "description": "Agent-first terminal workspace for Nolo",
6
- "bin": {
7
- "nolo": "./index.ts"
8
- },
9
- "module": "index.ts",
6
+ "bin": {
7
+ "nolo": "index.ts"
8
+ },
9
+ "module": "index.ts",
10
10
  "files": [
11
11
  "index.ts",
12
12
  "authCommands.ts",
13
13
  "commandRegistry.ts",
14
+ "updateCommands.ts",
14
15
  "client/agentRun.ts",
15
16
  "client/profileConfig.ts",
16
17
  "tui/readlineWorkspace.ts",
17
18
  "tui/session.ts",
18
19
  "README.md"
19
20
  ],
20
- "publishConfig": {
21
- "access": "public"
22
- },
23
- "devDependencies": {
24
- "bun-types": "latest"
25
- },
26
- "peerDependencies": {
27
- "typescript": "^5.0.0"
28
- }
29
- }
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "devDependencies": {
25
+ "bun-types": "latest"
26
+ },
27
+ "peerDependencies": {
28
+ "typescript": "^5.0.0"
29
+ }
30
+ }
@@ -1,6 +1,6 @@
1
1
  import { createInterface } from "node:readline";
2
2
  import { stdin as defaultInput, stdout as defaultOutput } from "node:process";
3
- import { runAgentTurn } from "../client/agentRun";
3
+ import { runAgentTurn, type RunAgentTurnResult } from "../client/agentRun";
4
4
  import {
5
5
  createInitialTuiState,
6
6
  handleTuiInput,
@@ -15,6 +15,7 @@ type WorkspaceOptions = {
15
15
  env?: NodeJS.ProcessEnv;
16
16
  input?: NodeJS.ReadableStream;
17
17
  output?: NodeJS.WritableStream;
18
+ agentRunner?: typeof runAgentTurn;
18
19
  };
19
20
 
20
21
  async function runAgentChat(
@@ -22,18 +23,20 @@ async function runAgentChat(
22
23
  state: TuiState,
23
24
  message: string,
24
25
  env: NodeJS.ProcessEnv,
25
- output: NodeJS.WritableStream
26
+ output: NodeJS.WritableStream,
27
+ agentRunner: typeof runAgentTurn = runAgentTurn
26
28
  ) {
27
- const exitCode = await runAgentTurn({
29
+ const result: RunAgentTurnResult = await agentRunner({
28
30
  agentName: state.agentName,
29
31
  agentKey: state.agentKey,
30
32
  serverUrl: state.serverUrl,
31
33
  message,
34
+ continueDialogId: state.dialogId,
32
35
  scriptDir,
33
36
  env,
34
37
  output,
35
38
  });
36
- return exitCode;
39
+ return result;
37
40
  }
38
41
 
39
42
  export async function startTuiWorkspace(options: WorkspaceOptions) {
@@ -60,13 +63,21 @@ export async function startTuiWorkspace(options: WorkspaceOptions) {
60
63
  }
61
64
 
62
65
  if (result.action?.type === "chat") {
63
- await runAgentChat(
66
+ const runResult = await runAgentChat(
64
67
  options.scriptDir,
65
68
  state,
66
69
  result.action.message,
67
70
  options.env ?? process.env,
68
- output
71
+ output,
72
+ options.agentRunner
69
73
  );
74
+ if (runResult.dialogId) {
75
+ state = {
76
+ ...state,
77
+ dialogId: runResult.dialogId,
78
+ dialogLabel: runResult.dialogId,
79
+ };
80
+ }
70
81
  }
71
82
 
72
83
  output.write(`\n${renderStatusLine(state)}\n`);
package/tui/session.ts CHANGED
@@ -1,4 +1,4 @@
1
- export const DEFAULT_TUI_AGENT_KEY = "agent-pub-01APPBUILDER00000001YAII3I";
1
+ export const DEFAULT_TUI_AGENT_KEY = "agent-pub-01NOLOAPPBLD000000019KCKT0";
2
2
  export const DEFAULT_TUI_SERVER_URL = "http://127.0.0.1:38123";
3
3
 
4
4
  const KNOWN_AGENT_ALIASES: Record<string, { name: string; key: string }> = {
@@ -12,12 +12,26 @@ const KNOWN_AGENT_ALIASES: Record<string, { name: string; key: string }> = {
12
12
  },
13
13
  };
14
14
 
15
+ function shortenDialogId(dialogId: string) {
16
+ return dialogId.length > 12
17
+ ? `${dialogId.slice(0, 6)}...${dialogId.slice(-4)}`
18
+ : dialogId;
19
+ }
20
+
21
+ export function resolveDialogLabel(
22
+ state: Pick<TuiState, "dialogId" | "dialogLabel">
23
+ ) {
24
+ return state.dialogId ? shortenDialogId(state.dialogId) : state.dialogLabel;
25
+ }
26
+
15
27
  export type TuiState = {
16
28
  agentKey: string;
17
29
  agentName: string;
30
+ dialogId?: string;
18
31
  dialogLabel: string;
19
32
  profileName: string;
20
33
  serverUrl: string;
34
+ cliVersion?: string;
21
35
  attachedDocs: string[];
22
36
  };
23
37
 
@@ -26,6 +40,7 @@ export type TuiAction =
26
40
  type: "chat";
27
41
  message: string;
28
42
  agentKey: string;
43
+ continueDialogId?: string;
29
44
  }
30
45
  | {
31
46
  type: "exit";
@@ -41,14 +56,16 @@ type EnvLike = Record<string, string | undefined>;
41
56
 
42
57
  export function createInitialTuiState(env: EnvLike = process.env): TuiState {
43
58
  const agentKey = env.NOLO_AGENT?.trim() || DEFAULT_TUI_AGENT_KEY;
44
- const agentName = env.NOLO_AGENT_NAME?.trim() || "app-builder";
59
+ const agentName = env.NOLO_AGENT_NAME?.trim() || "nolo";
45
60
 
46
61
  return {
47
62
  agentKey,
48
63
  agentName,
64
+ dialogId: env.NOLO_DIALOG_ID?.trim() || undefined,
49
65
  dialogLabel: env.NOLO_DIALOG?.trim() || "new",
50
66
  profileName: env.NOLO_PROFILE?.trim() || "local",
51
67
  serverUrl: (env.NOLO_SERVER || env.BASE_URL || DEFAULT_TUI_SERVER_URL).replace(/\/+$/, ""),
68
+ cliVersion: env.NOLO_CLI_VERSION?.trim() || undefined,
52
69
  attachedDocs: [],
53
70
  };
54
71
  }
@@ -61,9 +78,10 @@ export function renderStatusLine(state: TuiState) {
61
78
 
62
79
  return [
63
80
  `agent ${state.agentName}`,
64
- `dialog ${state.dialogLabel}`,
81
+ `dialog ${resolveDialogLabel(state)}`,
65
82
  `docs ${docs}`,
66
83
  `profile ${state.profileName}`,
84
+ ...(state.cliVersion ? [`version ${state.cliVersion}`] : []),
67
85
  ].join(" | ");
68
86
  }
69
87
 
@@ -73,12 +91,13 @@ export function renderWelcome(state: TuiState) {
73
91
  "Nolo workspace",
74
92
  "--------------",
75
93
  `agent ${state.agentName}`,
76
- `dialog ${state.dialogLabel}`,
94
+ `dialog ${resolveDialogLabel(state)}`,
77
95
  `docs ${state.attachedDocs.length ? state.attachedDocs.join(", ") : "none"}`,
78
96
  `profile ${state.profileName}`,
79
97
  `server ${state.serverUrl}`,
98
+ ...(state.cliVersion ? [`version ${state.cliVersion}`] : []),
80
99
  "",
81
- "Tell nolo what you want. Use /help for commands.",
100
+ "Tell nolo what you want. Use /help for commands. Use /version if this install feels stale.",
82
101
  "",
83
102
  ].join("\n");
84
103
  }
@@ -91,7 +110,7 @@ export function renderTuiHelp() {
91
110
  return [
92
111
  "Commands:",
93
112
  " /help Show this help",
94
- " /new Start a fresh dialog label",
113
+ " /new Start a fresh dialog",
95
114
  " /agent Show the current agent",
96
115
  " /switch <agent> Switch the current agent",
97
116
  " /dialog Show the current dialog",
@@ -100,6 +119,7 @@ export function renderTuiHelp() {
100
119
  " /customize Describe how you want to tune nolo",
101
120
  " /login Show login/profile hint",
102
121
  " /profile Show active profile",
122
+ " /version Show version/update hint",
103
123
  " /exit Leave the workspace",
104
124
  "",
105
125
  "You can also type normally. nolo will send it to the current agent.",
@@ -131,6 +151,7 @@ export function handleTuiInput(input: string, state: TuiState): TuiInputResult {
131
151
  type: "chat",
132
152
  message: trimmed,
133
153
  agentKey: state.agentKey,
154
+ ...(state.dialogId ? { continueDialogId: state.dialogId } : {}),
134
155
  },
135
156
  };
136
157
  }
@@ -146,8 +167,13 @@ export function handleTuiInput(input: string, state: TuiState): TuiInputResult {
146
167
  return { nextState: state, output: "Bye.", action: { type: "exit" } };
147
168
  case "/new":
148
169
  return {
149
- nextState: { ...state, dialogLabel: "new", attachedDocs: [] },
150
- output: "Started a fresh workspace.",
170
+ nextState: {
171
+ ...state,
172
+ dialogId: undefined,
173
+ dialogLabel: "new",
174
+ attachedDocs: [],
175
+ },
176
+ output: "Started a fresh dialog.",
151
177
  };
152
178
  case "/agent":
153
179
  return {
@@ -182,7 +208,9 @@ export function handleTuiInput(input: string, state: TuiState): TuiInputResult {
182
208
  case "/dialog":
183
209
  return {
184
210
  nextState: state,
185
- output: `Current dialog: ${state.dialogLabel}`,
211
+ output: state.dialogId
212
+ ? `Current dialog: ${state.dialogId}`
213
+ : "Current dialog: new",
186
214
  };
187
215
  case "/doc": {
188
216
  if (rest[0] === "attach") {
@@ -223,6 +251,14 @@ export function handleTuiInput(input: string, state: TuiState): TuiInputResult {
223
251
  nextState: state,
224
252
  output: `Profile: ${state.profileName}`,
225
253
  };
254
+ case "/version":
255
+ return {
256
+ nextState: state,
257
+ output:
258
+ `nolo ${state.cliVersion || "unknown version"}\n` +
259
+ "Update this install with: nolo update\n" +
260
+ "If repo-local output differs, publish/install the latest npm package first.",
261
+ };
226
262
  default:
227
263
  return {
228
264
  nextState: state,
@@ -0,0 +1,69 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ type PackageInfo = {
6
+ name: string;
7
+ version: string;
8
+ };
9
+
10
+ type DoctorInfo = {
11
+ packageName: string;
12
+ version: string;
13
+ entrypoint: string;
14
+ serverUrl: string;
15
+ profileName: string;
16
+ };
17
+
18
+ const CLI_DIR = dirname(fileURLToPath(import.meta.url));
19
+ const PACKAGE_JSON_PATH = join(CLI_DIR, "package.json");
20
+
21
+ export function readPackageInfo(): PackageInfo {
22
+ const raw = readFileSync(PACKAGE_JSON_PATH, "utf8");
23
+ const parsed = JSON.parse(raw) as Partial<PackageInfo>;
24
+ return {
25
+ name: parsed.name || "nolo-cli",
26
+ version: parsed.version || "0.0.0",
27
+ };
28
+ }
29
+
30
+ export function buildCliVersionText(info: PackageInfo) {
31
+ return `${info.name} ${info.version}`;
32
+ }
33
+
34
+ export function buildSelfUpdateCommand() {
35
+ return ["npm", "install", "-g", "nolo-cli@latest", "--force"];
36
+ }
37
+
38
+ export function buildCliDoctorText(info: DoctorInfo) {
39
+ const updateCommand = buildSelfUpdateCommand().join(" ");
40
+ return [
41
+ "Nolo CLI doctor",
42
+ "---------------",
43
+ `version ${info.packageName} ${info.version}`,
44
+ `entry ${info.entrypoint}`,
45
+ `server ${info.serverUrl}`,
46
+ `profile ${info.profileName}`,
47
+ `update nolo update`,
48
+ "",
49
+ "If direct `nolo` differs from repo-local `bun ./packages/cli/index.ts`,",
50
+ "the global npm install is older than this checkout.",
51
+ "",
52
+ `Manual update: ${updateCommand}`,
53
+ ].join("\n");
54
+ }
55
+
56
+ export async function runSelfUpdate(output: NodeJS.WritableStream = process.stdout) {
57
+ const command = buildSelfUpdateCommand();
58
+ output.write(`Updating nolo with: ${command.join(" ")}\n`);
59
+
60
+ const proc = Bun.spawn({
61
+ cmd: command,
62
+ stdin: "inherit",
63
+ stdout: "inherit",
64
+ stderr: "inherit",
65
+ env: process.env,
66
+ });
67
+
68
+ return await proc.exited;
69
+ }