@truss-harness/cli 0.1.0 → 0.1.2

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
@@ -23,13 +23,17 @@ truss-cli
23
23
  truss-cli help
24
24
  ```
25
25
 
26
+ ## Interactive setup
27
+
28
+ Run truss-cli setup after installation to discover local servers, choose a model, and save a default user-level profile. It is a command rather than an install-time prompt, so npm installs remain safe in CI and scripts.
29
+
26
30
  ## First run
27
31
 
28
32
  Start your local model server, open a terminal in the project you want Truss to work on, and run:
29
33
 
30
34
  ```sh
31
35
  truss-cli models
32
- truss-cli chat "Explain this workspace"
36
+ truss-cli chat --mode edit "Explain this workspace"
33
37
  ```
34
38
 
35
39
  Truss probes the standard Ollama, LM Studio, and llama.cpp endpoints when no model is configured. Create a reusable workspace profile when you want explicit settings:
@@ -50,6 +54,16 @@ truss-cli chat --internet-access "Check the current library documentation"
50
54
 
51
55
  Direct CLI chat is non-interactive and auto-allows registered tools. Run it only in a trusted workspace. Public internet tools are disabled unless `--internet-access`, a profile, or the environment explicitly enables them.
52
56
 
57
+ ## Persistent chat
58
+
59
+ Run `truss-cli chat` without a prompt to keep one local conversation open. The configuration flags supplied when it starts remain active, and history remains available as you send messages.
60
+
61
+ ```sh
62
+ truss-cli chat --mode edit --permission auto-read
63
+ ```
64
+
65
+ Use `:mode chat`, `:mode plan`, or `:mode edit` to change the active mode. You can also prefix a message with `--mode edit`, for example `--mode edit Inspect the repository and explain it.` The mode change persists for the rest of that conversation. Use `:clear` for a fresh conversation, `:help` for controls, and `:exit` to close it.
66
+
53
67
  ## Workspace commands
54
68
 
55
69
  These deterministic commands work without a configured model:
package/dist/bin.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { cwd } from "node:process";
3
+ import { createInterface } from "node:readline/promises";
3
4
  import { detectLocalEndpoints, listLocalModels } from "@truss-harness/provider-openai-compatible";
4
5
  import { brand } from "@truss-harness/branding";
5
6
  import { executeWorkspaceCommand, workspaceCommandHelp } from "@truss-harness/runtime";
6
7
  import { createClientRuntime } from "./runtime.js";
7
- import { configurationPaths, initializeWorkspaceConfiguration, parseConfigurationOverrides, resolveConfiguration } from "./config.js";
8
+ import { configurationPaths, initializeWorkspaceConfiguration, parseConfigurationOverrides, resolveConfiguration, saveUserProfile } from "./config.js";
8
9
  import { ProtocolToolApproval, runService } from "./protocol.js";
9
10
  const help = `${brand.productName} CLI
10
11
 
@@ -16,11 +17,14 @@ Usage:
16
17
  Quick start:
17
18
  1. Start Ollama, LM Studio, llama.cpp, or a compatible local server.
18
19
  2. Run: ${brand.cliCommand} models
19
- 3. Run: ${brand.cliCommand} chat "Explain this workspace"
20
- 4. Optional: ${brand.cliCommand} config init
20
+ 3. Run: ${brand.cliCommand} chat "Explain this workspace"
21
+ 3. Run: ${brand.cliCommand} setup
22
+ 4. Run: ${brand.cliCommand} chat --mode edit
23
+ 5. Optional: ${brand.cliCommand} config init
21
24
 
22
25
  Commands:
23
- chat <prompt> Stream one agent response in the current workspace
26
+ chat [prompt] Stream one response or open persistent chat
27
+ setup Interactively choose and save local-model defaults
24
28
  models List detected local servers and models
25
29
  config path Print user and workspace configuration paths
26
30
  config init Create a workspace configuration template
@@ -74,6 +78,149 @@ Examples:
74
78
 
75
79
  ${workspaceCommandHelp()}
76
80
  `;
81
+ function subscribeToRuntimeEvents(events) {
82
+ events.subscribe((event) => {
83
+ if (event.type === "text_delta")
84
+ process.stdout.write(event.text);
85
+ if (event.type === "tool_call_requested")
86
+ process.stderr.write("\n[tool] " + event.tool + "\n");
87
+ if (event.type === "plan_updated") {
88
+ const steps = event.plan.steps.map((step) => " " + (step.status === "completed" ? "[x]" : step.status === "in_progress" ? "[..]" : "[ ]") + " " + step.content).join("\n");
89
+ process.stderr.write("\n[plan] " + event.plan.title + "\n" + steps + "\n");
90
+ }
91
+ });
92
+ }
93
+ function inlineMode(input) {
94
+ const match = input.trim().match(/^--mode\s+(chat|plan|edit)(?:\s+([\s\S]*))?$/);
95
+ return match ? { mode: match[1], prompt: match[2]?.trim() ?? "" } : { prompt: input.trim() };
96
+ }
97
+ async function runInteractiveChat(initialConfiguration) {
98
+ let configuration = initialConfiguration;
99
+ let client = await createClientRuntime(configuration);
100
+ let session = await client.runtime.createSession();
101
+ let messages = session.messages;
102
+ subscribeToRuntimeEvents(client.events);
103
+ const readline = createInterface({ input: process.stdin, output: process.stdout, terminal: process.stdin.isTTY === true });
104
+ process.stdout.write(brand.productName + " interactive chat. :help for controls. Mode: " + configuration.mode + ".\n");
105
+ const replaceRuntime = async (mode) => {
106
+ const current = await client.runtime.getSession(session.id);
107
+ messages = current?.messages ?? messages;
108
+ await client.dispose();
109
+ configuration = { ...configuration, mode };
110
+ client = await createClientRuntime(configuration);
111
+ session = await client.runtime.createSession(messages);
112
+ subscribeToRuntimeEvents(client.events);
113
+ process.stdout.write("\n[mode: " + mode + "]\n");
114
+ };
115
+ try {
116
+ while (true) {
117
+ const line = await readline.question(brand.cliCommand + " (" + configuration.mode + ") > ").catch(() => undefined);
118
+ if (line === undefined)
119
+ break;
120
+ const input = line.trim();
121
+ if (!input)
122
+ continue;
123
+ if (input === ":exit" || input === ":quit")
124
+ break;
125
+ if (input === ":help") {
126
+ process.stdout.write("Controls: :mode chat|plan|edit, :clear, :exit. You can also prefix a message with --mode chat|plan|edit.\n");
127
+ continue;
128
+ }
129
+ if (input === ":clear") {
130
+ session = await client.runtime.createSession();
131
+ messages = session.messages;
132
+ process.stdout.write("[conversation cleared]\n");
133
+ continue;
134
+ }
135
+ const modeCommand = input.match(/^:mode\s+(chat|plan|edit)$/);
136
+ if (modeCommand) {
137
+ await replaceRuntime(modeCommand[1]);
138
+ continue;
139
+ }
140
+ if (input.startsWith(":mode")) {
141
+ process.stdout.write("Use :mode chat, :mode plan, or :mode edit.\n");
142
+ continue;
143
+ }
144
+ const next = inlineMode(input);
145
+ if (next.mode && next.mode !== configuration.mode)
146
+ await replaceRuntime(next.mode);
147
+ if (!next.prompt)
148
+ continue;
149
+ const workspaceCommand = await executeWorkspaceCommand({ workspaceRoot: cwd(), input: next.prompt });
150
+ if (workspaceCommand.handled) {
151
+ process.stdout.write(workspaceCommand.message + "\n");
152
+ continue;
153
+ }
154
+ try {
155
+ await client.runtime.run(session.id, next.prompt);
156
+ process.stdout.write("\n");
157
+ }
158
+ catch (error) {
159
+ process.stderr.write("\n" + (error instanceof Error ? error.message : String(error)) + "\n");
160
+ }
161
+ }
162
+ }
163
+ finally {
164
+ readline.close();
165
+ await client.dispose();
166
+ }
167
+ }
168
+ function selectedIndex(input, count, fallback) {
169
+ const index = Number.parseInt(input, 10);
170
+ return Number.isInteger(index) && index >= 1 && index <= count ? index - 1 : fallback;
171
+ }
172
+ async function runSetup() {
173
+ const readline = createInterface({ input: process.stdin, output: process.stdout, terminal: process.stdin.isTTY === true });
174
+ const ask = async (label, fallback) => {
175
+ const answer = (await readline.question(label + " [" + fallback + "]: ")).trim();
176
+ return answer || fallback;
177
+ };
178
+ try {
179
+ const endpoints = await detectLocalEndpoints();
180
+ process.stdout.write(brand.productName + " setup\n");
181
+ process.stdout.write("Choose a local model server. Values in brackets are defaults.\n\n");
182
+ endpoints.forEach((endpoint, index) => process.stdout.write(String(index + 1) + ". " + endpoint.label + " (" + endpoint.baseUrl + ")\n"));
183
+ process.stdout.write(String(endpoints.length + 1) + ". Custom endpoint\n");
184
+ const endpointChoice = selectedIndex(await ask("Server", "1"), endpoints.length + 1, 0);
185
+ let endpoint;
186
+ if (endpointChoice < endpoints.length) {
187
+ endpoint = endpoints[endpointChoice];
188
+ }
189
+ else {
190
+ const provider = await ask("Provider: ollama or openai-compatible", "openai-compatible");
191
+ if (provider !== "ollama" && provider !== "openai-compatible")
192
+ throw new Error("Provider must be ollama or openai-compatible.");
193
+ const fallbackUrl = provider === "ollama" ? "http://127.0.0.1:11434" : "http://127.0.0.1:1234/v1";
194
+ endpoint = { id: "custom", label: "Custom endpoint", kind: provider, baseUrl: await ask("Endpoint URL", fallbackUrl) };
195
+ }
196
+ const models = await listLocalModels(endpoint).catch(() => []);
197
+ models.forEach((model, index) => process.stdout.write(String(index + 1) + ". " + model.name + "\n"));
198
+ const model = models.length
199
+ ? models[selectedIndex(await ask("Model", "1"), models.length, 0)].name
200
+ : await ask("Model ID", "local-model");
201
+ const profileName = await ask("Profile name", endpoint.id === "custom" ? "local" : endpoint.id);
202
+ const mode = await ask("Default mode: chat, plan, or edit", "edit");
203
+ if (mode !== "chat" && mode !== "plan" && mode !== "edit")
204
+ throw new Error("Mode must be chat, plan, or edit.");
205
+ const permission = await ask("Default permission: ask, auto-read, or auto-all", "auto-read");
206
+ if (permission !== "ask" && permission !== "auto-read" && permission !== "auto-all")
207
+ throw new Error("Permission must be ask, auto-read, or auto-all.");
208
+ const internet = await ask("Enable internet research: yes or no", "no");
209
+ const path = await saveUserProfile(cwd(), profileName, {
210
+ provider: endpoint.kind,
211
+ baseUrl: endpoint.baseUrl,
212
+ model,
213
+ mode,
214
+ permission,
215
+ internetAccess: /^(y|yes|true|1)$/i.test(internet)
216
+ });
217
+ process.stdout.write("\nSaved profile '" + profileName + "' to " + path + "\n");
218
+ process.stdout.write("Start a persistent session with: " + brand.cliCommand + " chat --profile " + profileName + "\n");
219
+ }
220
+ finally {
221
+ readline.close();
222
+ }
223
+ }
77
224
  async function main() {
78
225
  const [command = "help", ...rawArgs] = process.argv.slice(2);
79
226
  if (command === "help" || command === "--help" || command === "-h") {
@@ -86,6 +233,10 @@ async function main() {
86
233
  process.stdout.write(`${JSON.stringify(models, null, 2)}\n`);
87
234
  return;
88
235
  }
236
+ if (command === "setup") {
237
+ await runSetup();
238
+ return;
239
+ }
89
240
  if (command === "config") {
90
241
  const [action = "path"] = rawArgs;
91
242
  const paths = configurationPaths(cwd());
@@ -113,7 +264,7 @@ async function main() {
113
264
  process.exitCode = 1;
114
265
  return;
115
266
  }
116
- if (command === "chat") {
267
+ if (command === "chat" && args.length) {
117
268
  const prompt = args.join(" ").trim();
118
269
  if (!prompt)
119
270
  throw new Error(`Provide a prompt: ${brand.cliCommand} chat <prompt>`);
@@ -140,6 +291,10 @@ async function main() {
140
291
  }
141
292
  return;
142
293
  }
294
+ if (command === "chat" && !args.join(" ").trim()) {
295
+ await runInteractiveChat(configuration);
296
+ return;
297
+ }
143
298
  const client = await createClientRuntime(configuration);
144
299
  const { runtime, events } = client;
145
300
  for (const server of client.mcpServers) {
package/dist/config.d.ts CHANGED
@@ -43,6 +43,8 @@ export declare function resolveConfiguration(options: {
43
43
  readonly paths?: ConfigurationPaths;
44
44
  }): Promise<ResolvedConfiguration>;
45
45
  export declare function initializeWorkspaceConfiguration(workspaceRoot: string, paths?: ConfigurationPaths): Promise<string>;
46
+ /** Saves a selected local-model profile in the user configuration file. */
47
+ export declare function saveUserProfile(workspaceRoot: string, profileName: string, profile: ProfileConfiguration, paths?: ConfigurationPaths): Promise<string>;
46
48
  export declare function parseConfigurationOverrides(arguments_: readonly string[]): {
47
49
  readonly overrides: ConfigurationOverrides;
48
50
  readonly rest: readonly string[];
package/dist/config.js CHANGED
@@ -208,6 +208,20 @@ export async function initializeWorkspaceConfiguration(workspaceRoot, paths = co
208
208
  }, null, 2)}\n`, "utf8");
209
209
  return paths.workspace;
210
210
  }
211
+ /** Saves a selected local-model profile in the user configuration file. */
212
+ export async function saveUserProfile(workspaceRoot, profileName, profile, paths = configurationPaths(workspaceRoot)) {
213
+ const existing = await readConfiguration(paths.user);
214
+ await mkdir(dirname(paths.user), { recursive: true });
215
+ await writeFile(paths.user, JSON.stringify({
216
+ ...existing,
217
+ defaultProfile: profileName,
218
+ profiles: {
219
+ ...existing.profiles,
220
+ [profileName]: profile
221
+ }
222
+ }, null, 2) + "\n", "utf8");
223
+ return paths.user;
224
+ }
211
225
  export function parseConfigurationOverrides(arguments_) {
212
226
  const overrides = {};
213
227
  const rest = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truss-harness/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Command-line client and runtime service for Truss.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -14,7 +14,7 @@
14
14
  }
15
15
  },
16
16
  "bin": {
17
- "truss-cli": "./dist/bin.js"
17
+ "truss-cli": "dist/bin.js"
18
18
  },
19
19
  "scripts": {
20
20
  "build": "tsc -b",
@@ -3,9 +3,8 @@ if (process.env.npm_config_global === "true" || process.env.npm_config_global ==
3
3
  Truss CLI installed.
4
4
 
5
5
  Next steps:
6
- truss-cli models
7
- truss-cli config init
8
- truss-cli chat "Explain this workspace"
6
+ truss-cli setup
7
+ truss-cli chat --mode edit
9
8
 
10
9
  Run truss-cli help for modes, permissions, configuration, and examples.
11
10
  `);