@truss-harness/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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Truss contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # Truss CLI
2
+
3
+ <p align="center"><img src="./logo.png" width="96" alt="Truss logo"></p>
4
+
5
+ Truss is a local-first coding-agent runtime. The CLI runs one-shot agent tasks and hosts the runtime service used by editor clients.
6
+
7
+ ## Requirements
8
+
9
+ - Node.js 20 or newer
10
+ - Ollama, LM Studio, llama.cpp, or another compatible local model server
11
+ - A model with native tool calling for Agent and Plan workflows
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ npm install -g @truss-harness/cli
17
+ ```
18
+
19
+ The global installer prints a short first-run checklist. You can display the complete reference at any time:
20
+
21
+ ```sh
22
+ truss-cli
23
+ truss-cli help
24
+ ```
25
+
26
+ ## First run
27
+
28
+ Start your local model server, open a terminal in the project you want Truss to work on, and run:
29
+
30
+ ```sh
31
+ truss-cli models
32
+ truss-cli chat "Explain this workspace"
33
+ ```
34
+
35
+ 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:
36
+
37
+ ```sh
38
+ truss-cli config init
39
+ truss-cli config path
40
+ ```
41
+
42
+ ## Agent tasks
43
+
44
+ ```sh
45
+ truss-cli chat --mode chat "Explain the architecture"
46
+ truss-cli chat --mode plan "Plan an authentication refactor"
47
+ truss-cli chat --mode edit "Fix the failing tests"
48
+ truss-cli chat --internet-access "Check the current library documentation"
49
+ ```
50
+
51
+ 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
+
53
+ ## Workspace commands
54
+
55
+ These deterministic commands work without a configured model:
56
+
57
+ ```text
58
+ truss-cli init Create or refresh generated AGENTS.md context
59
+ truss-cli update [note] Record Git state and a durable handoff note
60
+ truss-cli status Show Git state and recent durable records
61
+ truss-cli clear-memory Remove durable workspace memory
62
+ truss-cli commands Show slash commands used by interactive clients
63
+ ```
64
+
65
+ ## Configuration
66
+
67
+ `truss-cli config init` creates `.truss-harness/config.json`. Profiles support `provider`, `baseUrl`, `model`, `mode`, `permission`, `internetAccess`, `systemPrompt`, `apiKeyEnv`, and `mcpServers`.
68
+
69
+ ```json
70
+ {
71
+ "defaultProfile": "ollama",
72
+ "profiles": {
73
+ "ollama": {
74
+ "provider": "ollama",
75
+ "baseUrl": "http://127.0.0.1:11434",
76
+ "model": "qwen3:8b",
77
+ "mode": "edit",
78
+ "permission": "ask",
79
+ "internetAccess": false
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ Keep endpoint tokens out of JSON. Set the token in an environment variable and place that variable's name in `apiKeyEnv`.
86
+
87
+ MCP stdio servers can be defined under `mcpServers`. User-level definitions load normally. Workspace definitions can launch local processes and are ignored unless the user configuration sets `"allowWorkspaceMcpServers": true`. Plan mode loads only servers marked `"readOnly": true`; Agent mode loads all enabled servers. MCP calls follow the selected approval policy.
88
+
89
+ ## Service mode
90
+
91
+ `truss-cli serve` starts the newline-delimited JSON runtime protocol used by clients such as the VS Code extension. Standard output is reserved for protocol messages.
92
+
93
+ Run `truss-cli help` for every command, flag, environment variable, mode, permission policy, and example.
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+ import { cwd } from "node:process";
3
+ import { detectLocalEndpoints, listLocalModels } from "@truss-harness/provider-openai-compatible";
4
+ import { brand } from "@truss-harness/branding";
5
+ import { executeWorkspaceCommand, workspaceCommandHelp } from "@truss-harness/runtime";
6
+ import { createClientRuntime } from "./runtime.js";
7
+ import { configurationPaths, initializeWorkspaceConfiguration, parseConfigurationOverrides, resolveConfiguration } from "./config.js";
8
+ import { ProtocolToolApproval, runService } from "./protocol.js";
9
+ const help = `${brand.productName} CLI
10
+
11
+ Local-first coding agent and runtime service.
12
+
13
+ Usage:
14
+ ${brand.cliCommand} <command> [options]
15
+
16
+ Quick start:
17
+ 1. Start Ollama, LM Studio, llama.cpp, or a compatible local server.
18
+ 2. Run: ${brand.cliCommand} models
19
+ 3. Run: ${brand.cliCommand} chat "Explain this workspace"
20
+ 4. Optional: ${brand.cliCommand} config init
21
+
22
+ Commands:
23
+ chat <prompt> Stream one agent response in the current workspace
24
+ models List detected local servers and models
25
+ config path Print user and workspace configuration paths
26
+ config init Create a workspace configuration template
27
+ init Create or refresh generated AGENTS.md workspace context
28
+ update [note] Record Git state and a durable handoff note
29
+ status Show Git state and recent durable agent records
30
+ clear-memory Delete this workspace's durable agent memory
31
+ commands Print slash commands shared by interactive clients
32
+ serve Start the JSONL runtime service for editor clients
33
+ help Show this reference
34
+
35
+ Options:
36
+ --profile <name> Select a named configuration profile
37
+ --provider <name> ollama or openai-compatible
38
+ --base-url <url> Local server endpoint
39
+ --model <name> Model identifier
40
+ --mode <name> chat, plan, or edit
41
+ --permission <name> ask, auto-read, or auto-all
42
+ --internet-access Enable public web search and page fetching
43
+ --no-internet-access Disable internet tools
44
+
45
+ Modes:
46
+ chat Conversation only; optional internet tools
47
+ plan Read-only inspection and a saved implementation plan
48
+ edit Filesystem, planning, and terminal tools
49
+
50
+ Permissions:
51
+ ask Ask interactive clients before each tool
52
+ auto-read Auto-allow workspace reads; ask for writes, commands, and web
53
+ auto-all Auto-allow every registered tool
54
+
55
+ Direct '${brand.cliCommand} chat' runs non-interactively and auto-allows tools.
56
+ Use it only in a trusted workspace. Internet tools remain off unless enabled.
57
+
58
+ Environment:
59
+ TRUSS_HARNESS_MODEL Required model identifier
60
+ TRUSS_HARNESS_PROVIDER ollama (default) or openai-compatible
61
+ TRUSS_HARNESS_BASE_URL Local server base URL (default depends on provider)
62
+ TRUSS_HARNESS_AGENT_MODE chat (default), plan, or edit
63
+ TRUSS_HARNESS_PERMISSION_MODE ask (default), auto-read, or auto-all
64
+ TRUSS_HARNESS_INTERNET_ACCESS true or 1 to enable public web tools
65
+ TRUSS_HARNESS_API_KEY Optional bearer token
66
+ TRUSS_HARNESS_SYSTEM_PROMPT Optional system prompt
67
+ TRUSS_HARNESS_MCP_SERVERS JSON object containing local stdio MCP servers
68
+
69
+ Examples:
70
+ ${brand.cliCommand} chat --mode plan "Plan the authentication change"
71
+ ${brand.cliCommand} chat --mode edit "Fix the failing tests"
72
+ ${brand.cliCommand} chat --internet-access "Check the current library documentation"
73
+ ${brand.cliCommand} chat --profile lm-studio "Review the active file structure"
74
+
75
+ ${workspaceCommandHelp()}
76
+ `;
77
+ async function main() {
78
+ const [command = "help", ...rawArgs] = process.argv.slice(2);
79
+ if (command === "help" || command === "--help" || command === "-h") {
80
+ process.stdout.write(help);
81
+ return;
82
+ }
83
+ if (command === "models") {
84
+ const endpoints = await detectLocalEndpoints();
85
+ const models = await Promise.all(endpoints.map(async (endpoint) => ({ endpoint, models: await listLocalModels(endpoint) })));
86
+ process.stdout.write(`${JSON.stringify(models, null, 2)}\n`);
87
+ return;
88
+ }
89
+ if (command === "config") {
90
+ const [action = "path"] = rawArgs;
91
+ const paths = configurationPaths(cwd());
92
+ if (action === "path") {
93
+ process.stdout.write(`${JSON.stringify(paths, null, 2)}\n`);
94
+ return;
95
+ }
96
+ if (action === "init") {
97
+ process.stdout.write(`Created ${await initializeWorkspaceConfiguration(cwd(), paths)}\n`);
98
+ return;
99
+ }
100
+ throw new Error(`Use ${brand.cliCommand} config path or ${brand.cliCommand} config init`);
101
+ }
102
+ const { overrides, rest: args } = parseConfigurationOverrides(rawArgs);
103
+ const workspaceCommand = command === "init" ? "/init"
104
+ : command === "update" ? `/update ${args.join(" ")}`.trim()
105
+ : command === "status" ? "/status"
106
+ : command === "clear-memory" ? "/clear-memory"
107
+ : command === "commands" ? "/help"
108
+ : undefined;
109
+ if (workspaceCommand) {
110
+ const result = await executeWorkspaceCommand({ workspaceRoot: cwd(), input: workspaceCommand });
111
+ process.stdout.write(`${result.message}\n`);
112
+ if (!result.ok)
113
+ process.exitCode = 1;
114
+ return;
115
+ }
116
+ if (command === "chat") {
117
+ const prompt = args.join(" ").trim();
118
+ if (!prompt)
119
+ throw new Error(`Provide a prompt: ${brand.cliCommand} chat <prompt>`);
120
+ const result = await executeWorkspaceCommand({ workspaceRoot: cwd(), input: prompt });
121
+ if (result.handled) {
122
+ process.stdout.write(`${result.message}\n`);
123
+ if (!result.ok)
124
+ process.exitCode = 1;
125
+ return;
126
+ }
127
+ }
128
+ const configuration = await resolveConfiguration({ workspaceRoot: cwd(), overrides });
129
+ if (command === "serve") {
130
+ const approval = new ProtocolToolApproval(configuration.permission);
131
+ const client = await createClientRuntime({ ...configuration, approval });
132
+ for (const server of client.mcpServers) {
133
+ process.stderr.write(`[mcp] ${server.name}: ${server.state}${server.error ? ` (${server.error})` : ` (${server.toolCount} tools)`}\n`);
134
+ }
135
+ try {
136
+ await runService(client.runtime, client.events, approval);
137
+ }
138
+ finally {
139
+ await client.dispose();
140
+ }
141
+ return;
142
+ }
143
+ const client = await createClientRuntime(configuration);
144
+ const { runtime, events } = client;
145
+ for (const server of client.mcpServers) {
146
+ process.stderr.write(`[mcp] ${server.name}: ${server.state}${server.error ? ` (${server.error})` : ` (${server.toolCount} tools)`}\n`);
147
+ }
148
+ if (command === "chat") {
149
+ const prompt = args.join(" ").trim();
150
+ events.subscribe((event) => {
151
+ if (event.type === "text_delta")
152
+ process.stdout.write(event.text);
153
+ if (event.type === "tool_call_requested")
154
+ process.stderr.write(`\n[tool] ${event.tool}\n`);
155
+ if (event.type === "plan_updated") {
156
+ process.stderr.write(`\n[plan] ${event.plan.title}\n${event.plan.steps.map((step) => ` ${step.status === "completed" ? "[x]" : step.status === "in_progress" ? "[..]" : "[ ]"} ${step.content}`).join("\n")}\n`);
157
+ }
158
+ });
159
+ try {
160
+ const session = await runtime.createSession();
161
+ await runtime.run(session.id, prompt);
162
+ process.stdout.write("\n");
163
+ }
164
+ finally {
165
+ await client.dispose();
166
+ }
167
+ return;
168
+ }
169
+ throw new Error(`Unknown command: ${command}\n\n${help}`);
170
+ }
171
+ main().catch((error) => {
172
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
173
+ process.exitCode = 1;
174
+ });
@@ -0,0 +1,49 @@
1
+ import { type McpServerConfigurations } from "@truss-harness/mcp";
2
+ import { type LocalEndpointKind } from "@truss-harness/provider-openai-compatible";
3
+ import type { AgentMode, ClientConfiguration } from "./runtime.js";
4
+ import type { PermissionMode } from "./protocol.js";
5
+ export interface ProfileConfiguration {
6
+ readonly provider?: LocalEndpointKind;
7
+ readonly baseUrl?: string;
8
+ readonly model?: string;
9
+ readonly mode?: AgentMode;
10
+ readonly permission?: PermissionMode;
11
+ readonly internetAccess?: boolean;
12
+ readonly systemPrompt?: string;
13
+ /** Name of an environment variable containing a local endpoint token. */
14
+ readonly apiKeyEnv?: string;
15
+ readonly mcpServers?: McpServerConfigurations;
16
+ }
17
+ export interface HarnessConfiguration extends ProfileConfiguration {
18
+ readonly defaultProfile?: string;
19
+ readonly profiles?: Readonly<Record<string, ProfileConfiguration>>;
20
+ /** User-level opt-in for executable MCP definitions from workspace configuration. */
21
+ readonly allowWorkspaceMcpServers?: boolean;
22
+ }
23
+ export interface ConfigurationOverrides extends ProfileConfiguration {
24
+ readonly profile?: string;
25
+ }
26
+ export interface ResolvedConfiguration extends ClientConfiguration {
27
+ readonly profile?: string;
28
+ readonly permission: PermissionMode;
29
+ readonly paths: {
30
+ readonly user: string;
31
+ readonly workspace: string;
32
+ };
33
+ }
34
+ export interface ConfigurationPaths {
35
+ readonly user: string;
36
+ readonly workspace: string;
37
+ }
38
+ export declare function configurationPaths(workspaceRoot: string, environment?: NodeJS.ProcessEnv): ConfigurationPaths;
39
+ export declare function resolveConfiguration(options: {
40
+ readonly workspaceRoot: string;
41
+ readonly overrides?: ConfigurationOverrides;
42
+ readonly environment?: NodeJS.ProcessEnv;
43
+ readonly paths?: ConfigurationPaths;
44
+ }): Promise<ResolvedConfiguration>;
45
+ export declare function initializeWorkspaceConfiguration(workspaceRoot: string, paths?: ConfigurationPaths): Promise<string>;
46
+ export declare function parseConfigurationOverrides(arguments_: readonly string[]): {
47
+ readonly overrides: ConfigurationOverrides;
48
+ readonly rest: readonly string[];
49
+ };
package/dist/config.js ADDED
@@ -0,0 +1,256 @@
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { brand } from "@truss-harness/branding";
5
+ import { parseMcpServerConfigurations } from "@truss-harness/mcp";
6
+ import { detectActiveLocalModel } from "@truss-harness/provider-openai-compatible";
7
+ function validProvider(value) {
8
+ return value === "ollama" || value === "openai-compatible" ? value : undefined;
9
+ }
10
+ function validMode(value) {
11
+ return value === "chat" || value === "plan" || value === "edit" ? value : undefined;
12
+ }
13
+ function validPermission(value) {
14
+ return value === "ask" || value === "auto-read" || value === "auto-all" ? value : undefined;
15
+ }
16
+ function object(value) {
17
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
18
+ }
19
+ function parseProfile(value) {
20
+ const source = object(value);
21
+ if (!source)
22
+ return {};
23
+ return {
24
+ provider: validProvider(source.provider),
25
+ baseUrl: typeof source.baseUrl === "string" ? source.baseUrl : undefined,
26
+ model: typeof source.model === "string" ? source.model : undefined,
27
+ mode: validMode(source.mode),
28
+ permission: validPermission(source.permission),
29
+ internetAccess: typeof source.internetAccess === "boolean" ? source.internetAccess : undefined,
30
+ systemPrompt: typeof source.systemPrompt === "string" ? source.systemPrompt : undefined,
31
+ apiKeyEnv: typeof source.apiKeyEnv === "string" ? source.apiKeyEnv : undefined,
32
+ mcpServers: source.mcpServers === undefined ? undefined : parseMcpServerConfigurations(source.mcpServers)
33
+ };
34
+ }
35
+ function parseConfiguration(value) {
36
+ const source = object(value);
37
+ if (!source)
38
+ throw new Error("Configuration root must be a JSON object.");
39
+ const profilesSource = object(source.profiles);
40
+ const profiles = profilesSource
41
+ ? Object.fromEntries(Object.entries(profilesSource).map(([name, profile]) => [name, parseProfile(profile)]))
42
+ : undefined;
43
+ return {
44
+ ...parseProfile(source),
45
+ defaultProfile: typeof source.defaultProfile === "string" ? source.defaultProfile : undefined,
46
+ profiles,
47
+ allowWorkspaceMcpServers: source.allowWorkspaceMcpServers === true
48
+ };
49
+ }
50
+ async function readConfiguration(path) {
51
+ try {
52
+ return parseConfiguration(JSON.parse(await readFile(path, "utf8")));
53
+ }
54
+ catch (error) {
55
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
56
+ return {};
57
+ const message = error instanceof Error ? error.message : String(error);
58
+ throw new Error(`Unable to read ${brand.productName} configuration at ${path}: ${message}`);
59
+ }
60
+ }
61
+ function environmentConfiguration(environment) {
62
+ let mcpServers;
63
+ if (environment.TRUSS_HARNESS_MCP_SERVERS) {
64
+ try {
65
+ mcpServers = parseMcpServerConfigurations(JSON.parse(environment.TRUSS_HARNESS_MCP_SERVERS));
66
+ }
67
+ catch (error) {
68
+ throw new Error(`TRUSS_HARNESS_MCP_SERVERS must contain valid MCP server JSON: ${error instanceof Error ? error.message : String(error)}`);
69
+ }
70
+ }
71
+ return {
72
+ provider: validProvider(environment.TRUSS_HARNESS_PROVIDER),
73
+ baseUrl: environment.TRUSS_HARNESS_BASE_URL,
74
+ model: environment.TRUSS_HARNESS_MODEL,
75
+ mode: validMode(environment.TRUSS_HARNESS_AGENT_MODE),
76
+ permission: validPermission(environment.TRUSS_HARNESS_PERMISSION_MODE),
77
+ internetAccess: environment.TRUSS_HARNESS_INTERNET_ACCESS === undefined
78
+ ? undefined
79
+ : environment.TRUSS_HARNESS_INTERNET_ACCESS === "true" || environment.TRUSS_HARNESS_INTERNET_ACCESS === "1",
80
+ systemPrompt: environment.TRUSS_HARNESS_SYSTEM_PROMPT,
81
+ apiKeyEnv: environment.TRUSS_HARNESS_API_KEY ? "TRUSS_HARNESS_API_KEY" : undefined,
82
+ mcpServers
83
+ };
84
+ }
85
+ const profileKeys = [
86
+ "provider",
87
+ "baseUrl",
88
+ "model",
89
+ "mode",
90
+ "permission",
91
+ "internetAccess",
92
+ "systemPrompt",
93
+ "apiKeyEnv",
94
+ "mcpServers"
95
+ ];
96
+ function mergeProfiles(...sources) {
97
+ const merged = {};
98
+ for (const source of sources) {
99
+ if (!source)
100
+ continue;
101
+ for (const key of profileKeys) {
102
+ const value = source[key];
103
+ if (value !== undefined)
104
+ merged[key] = value;
105
+ }
106
+ }
107
+ return merged;
108
+ }
109
+ export function configurationPaths(workspaceRoot, environment = process.env) {
110
+ const userRoot = process.platform === "win32"
111
+ ? environment.APPDATA ?? join(homedir(), "AppData", "Roaming")
112
+ : environment.XDG_CONFIG_HOME ?? join(homedir(), ".config");
113
+ return {
114
+ user: join(userRoot, brand.productSlug, "config.json"),
115
+ workspace: join(workspaceRoot, brand.workspaceDirectory, "config.json")
116
+ };
117
+ }
118
+ export async function resolveConfiguration(options) {
119
+ const environment = options.environment ?? process.env;
120
+ const paths = options.paths ?? configurationPaths(options.workspaceRoot, environment);
121
+ const [user, workspace] = await Promise.all([readConfiguration(paths.user), readConfiguration(paths.workspace)]);
122
+ const profile = options.overrides?.profile ?? workspace.defaultProfile ?? user.defaultProfile ?? environment.TRUSS_HARNESS_PROFILE;
123
+ const userProfile = profile ? user.profiles?.[profile] : undefined;
124
+ const workspaceProfile = profile ? workspace.profiles?.[profile] : undefined;
125
+ const environmentProfile = environmentConfiguration(environment);
126
+ const merged = mergeProfiles(environmentProfile, user, userProfile, workspace, workspaceProfile, options.overrides);
127
+ const workspaceMcpServers = user.allowWorkspaceMcpServers
128
+ ? workspaceProfile?.mcpServers ?? workspace.mcpServers
129
+ : undefined;
130
+ const mcpServers = options.overrides?.mcpServers
131
+ ?? workspaceMcpServers
132
+ ?? userProfile?.mcpServers
133
+ ?? user.mcpServers
134
+ ?? environmentProfile.mcpServers;
135
+ let provider = merged.provider ?? "ollama";
136
+ let baseUrl = merged.baseUrl;
137
+ let model = merged.model;
138
+ if (!model) {
139
+ const hasConfiguredEndpoint = merged.provider !== undefined || baseUrl !== undefined;
140
+ const configuredEndpoint = hasConfiguredEndpoint ? [{
141
+ id: "configured",
142
+ label: "Configured endpoint",
143
+ kind: provider,
144
+ baseUrl: baseUrl ?? (provider === "ollama" ? "http://127.0.0.1:11434" : "http://127.0.0.1:1234/v1")
145
+ }] : undefined;
146
+ const detected = await detectActiveLocalModel({ endpoints: configuredEndpoint });
147
+ if (detected) {
148
+ provider = detected.endpoint.kind;
149
+ baseUrl = detected.endpoint.baseUrl;
150
+ model = detected.model.name;
151
+ }
152
+ }
153
+ if (!model)
154
+ throw new Error(`Set a model with --model, TRUSS_HARNESS_MODEL, a ${brand.productName} config profile, or start a local model server.`);
155
+ const apiKey = merged.apiKeyEnv ? environment[merged.apiKeyEnv] : undefined;
156
+ return {
157
+ workspaceRoot: options.workspaceRoot,
158
+ provider,
159
+ baseUrl: baseUrl ?? (provider === "ollama" ? "http://127.0.0.1:11434" : "http://127.0.0.1:1234/v1"),
160
+ model,
161
+ mode: merged.mode ?? "chat",
162
+ permission: merged.permission ?? "ask",
163
+ internetAccess: merged.internetAccess ?? false,
164
+ apiKey,
165
+ systemPrompt: merged.systemPrompt,
166
+ mcpServers,
167
+ profile,
168
+ paths
169
+ };
170
+ }
171
+ export async function initializeWorkspaceConfiguration(workspaceRoot, paths = configurationPaths(workspaceRoot)) {
172
+ try {
173
+ await access(paths.workspace);
174
+ throw new Error(`Configuration already exists at ${paths.workspace}`);
175
+ }
176
+ catch (error) {
177
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
178
+ throw error;
179
+ }
180
+ await mkdir(dirname(paths.workspace), { recursive: true });
181
+ await writeFile(paths.workspace, `${JSON.stringify({
182
+ defaultProfile: "ollama",
183
+ profiles: {
184
+ ollama: {
185
+ provider: "ollama",
186
+ baseUrl: "http://127.0.0.1:11434",
187
+ model: "qwen3:8b",
188
+ mode: "edit",
189
+ permission: "ask",
190
+ internetAccess: false
191
+ },
192
+ "lm-studio": {
193
+ provider: "openai-compatible",
194
+ baseUrl: "http://127.0.0.1:1234/v1",
195
+ model: "local-model-id",
196
+ mode: "plan",
197
+ permission: "auto-read"
198
+ }
199
+ },
200
+ mcpServers: {
201
+ filesystem: {
202
+ enabled: false,
203
+ command: "npx",
204
+ args: ["-y", "@modelcontextprotocol/server-filesystem", "."],
205
+ readOnly: false
206
+ }
207
+ }
208
+ }, null, 2)}\n`, "utf8");
209
+ return paths.workspace;
210
+ }
211
+ export function parseConfigurationOverrides(arguments_) {
212
+ const overrides = {};
213
+ const rest = [];
214
+ for (let index = 0; index < arguments_.length; index++) {
215
+ const argument = arguments_[index];
216
+ const value = () => {
217
+ const next = arguments_[++index];
218
+ if (!next || next.startsWith("--"))
219
+ throw new Error(`Missing value for ${argument}`);
220
+ return next;
221
+ };
222
+ if (argument === "--profile")
223
+ overrides.profile = value();
224
+ else if (argument === "--provider") {
225
+ const provider = validProvider(value());
226
+ if (!provider)
227
+ throw new Error("--provider must be ollama or openai-compatible");
228
+ overrides.provider = provider;
229
+ }
230
+ else if (argument === "--base-url")
231
+ overrides.baseUrl = value();
232
+ else if (argument === "--model")
233
+ overrides.model = value();
234
+ else if (argument === "--mode") {
235
+ const mode = validMode(value());
236
+ if (!mode)
237
+ throw new Error("--mode must be chat, plan, or edit");
238
+ overrides.mode = mode;
239
+ }
240
+ else if (argument === "--permission") {
241
+ const permission = validPermission(value());
242
+ if (!permission)
243
+ throw new Error("--permission must be ask, auto-read, or auto-all");
244
+ overrides.permission = permission;
245
+ }
246
+ else if (argument === "--internet-access") {
247
+ overrides.internetAccess = true;
248
+ }
249
+ else if (argument === "--no-internet-access") {
250
+ overrides.internetAccess = false;
251
+ }
252
+ else
253
+ rest.push(argument);
254
+ }
255
+ return { overrides, rest };
256
+ }
@@ -0,0 +1,50 @@
1
+ import type { AgentRuntime, ChatMessage, ContextBlock, RuntimeEvent, ToolApproval, ToolCall, Session } from "@truss-harness/runtime";
2
+ export type RuntimeServiceRequest = {
3
+ readonly type: "run";
4
+ readonly requestId: string;
5
+ readonly prompt: string;
6
+ readonly sessionId?: string;
7
+ readonly context?: readonly ContextBlock[];
8
+ } | {
9
+ readonly type: "create_session";
10
+ readonly requestId: string;
11
+ readonly messages?: readonly ChatMessage[];
12
+ } | {
13
+ readonly type: "abort";
14
+ readonly requestId: string;
15
+ } | {
16
+ readonly type: "tool_approval";
17
+ readonly requestId: string;
18
+ readonly callId: string;
19
+ readonly approved: boolean;
20
+ };
21
+ export type RuntimeServiceMessage = {
22
+ readonly type: "event";
23
+ readonly requestId: string;
24
+ readonly event: RuntimeEvent;
25
+ } | {
26
+ readonly type: "response";
27
+ readonly requestId: string;
28
+ readonly result: {
29
+ readonly sessionId?: string;
30
+ readonly aborted?: boolean;
31
+ };
32
+ } | {
33
+ readonly type: "error";
34
+ readonly requestId?: string;
35
+ readonly message: string;
36
+ };
37
+ export type PermissionMode = "ask" | "auto-read" | "auto-all";
38
+ /** Bridges runtime approval requests to a client using the JSONL service protocol. */
39
+ export declare class ProtocolToolApproval implements ToolApproval {
40
+ private readonly mode;
41
+ private readonly pending;
42
+ constructor(mode?: PermissionMode);
43
+ approve(call: ToolCall, _session: Session): Promise<boolean>;
44
+ resolve(callId: string, approved: boolean): boolean;
45
+ denyAll(): void;
46
+ }
47
+ /** Starts a newline-delimited JSON service suitable for editor clients and process isolation. */
48
+ export declare function runService(runtime: AgentRuntime, events: {
49
+ subscribe(listener: (event: RuntimeEvent) => void): () => void;
50
+ }, approval?: ProtocolToolApproval): Promise<void>;
@@ -0,0 +1,101 @@
1
+ import { createInterface } from "node:readline";
2
+ import { stdin, stdout } from "node:process";
3
+ const readOnlyTools = new Set(["read_file", "list_directory", "search_files", "grep"]);
4
+ function write(message) {
5
+ stdout.write(`${JSON.stringify(message)}\n`);
6
+ }
7
+ /** Bridges runtime approval requests to a client using the JSONL service protocol. */
8
+ export class ProtocolToolApproval {
9
+ mode;
10
+ pending = new Map();
11
+ constructor(mode = "ask") {
12
+ this.mode = mode;
13
+ }
14
+ approve(call, _session) {
15
+ if (this.mode === "auto-all")
16
+ return Promise.resolve(true);
17
+ if (this.mode === "auto-read" && readOnlyTools.has(call.name))
18
+ return Promise.resolve(true);
19
+ return new Promise((resolve) => this.pending.set(call.id, resolve));
20
+ }
21
+ resolve(callId, approved) {
22
+ const pending = this.pending.get(callId);
23
+ if (!pending)
24
+ return false;
25
+ this.pending.delete(callId);
26
+ pending(approved);
27
+ return true;
28
+ }
29
+ denyAll() {
30
+ for (const resolve of this.pending.values())
31
+ resolve(false);
32
+ this.pending.clear();
33
+ }
34
+ }
35
+ /** Starts a newline-delimited JSON service suitable for editor clients and process isolation. */
36
+ export async function runService(runtime, events, approval) {
37
+ const requestsBySession = new Map();
38
+ const controllers = new Map();
39
+ events.subscribe((event) => {
40
+ const requestId = requestsBySession.get(event.sessionId);
41
+ if (requestId)
42
+ write({ type: "event", requestId, event });
43
+ });
44
+ const lines = createInterface({ input: stdin, crlfDelay: Infinity });
45
+ for await (const line of lines) {
46
+ let request;
47
+ try {
48
+ request = JSON.parse(line);
49
+ }
50
+ catch {
51
+ write({ type: "error", message: "Invalid JSON request." });
52
+ continue;
53
+ }
54
+ if (request.type === "abort") {
55
+ const controller = controllers.get(request.requestId);
56
+ if (!controller)
57
+ write({ type: "error", requestId: request.requestId, message: "Unknown active request." });
58
+ else {
59
+ controller.abort();
60
+ approval?.denyAll();
61
+ write({ type: "response", requestId: request.requestId, result: { aborted: true } });
62
+ }
63
+ continue;
64
+ }
65
+ if (request.type === "tool_approval") {
66
+ approval?.resolve(request.callId, request.approved);
67
+ continue;
68
+ }
69
+ if (request.type === "create_session") {
70
+ const session = await runtime.createSession(request.messages ?? []);
71
+ write({ type: "response", requestId: request.requestId, result: { sessionId: session.id } });
72
+ continue;
73
+ }
74
+ if (request.type !== "run" || typeof request.requestId !== "string" || typeof request.prompt !== "string") {
75
+ write({ type: "error", requestId: request.requestId, message: "Invalid service request." });
76
+ continue;
77
+ }
78
+ const session = request.sessionId ? await runtime.getSession(request.sessionId) : await runtime.createSession();
79
+ if (!session) {
80
+ write({ type: "error", requestId: request.requestId, message: `Unknown session: ${request.sessionId}` });
81
+ continue;
82
+ }
83
+ const controller = new AbortController();
84
+ requestsBySession.set(session.id, request.requestId);
85
+ controllers.set(request.requestId, controller);
86
+ const requestContext = Array.isArray(request.context)
87
+ ? request.context.filter((block) => Boolean(block)
88
+ && typeof block === "object"
89
+ && typeof block.source === "string"
90
+ && typeof block.content === "string"
91
+ && (block.priority === undefined || typeof block.priority === "number"))
92
+ : [];
93
+ void runtime.run(session.id, request.prompt, controller.signal, requestContext)
94
+ .then(() => write({ type: "response", requestId: request.requestId, result: { sessionId: session.id } }))
95
+ .catch((error) => write({ type: "error", requestId: request.requestId, message: error instanceof Error ? error.message : String(error) }))
96
+ .finally(() => {
97
+ requestsBySession.delete(session.id);
98
+ controllers.delete(request.requestId);
99
+ });
100
+ }
101
+ }
@@ -0,0 +1,26 @@
1
+ import { type LocalEndpointKind } from "@truss-harness/provider-openai-compatible";
2
+ import { type McpServerConfigurations, type McpServerStatus } from "@truss-harness/mcp";
3
+ import { AgentRuntime, EventBus, type ToolApproval, type RuntimeEvent } from "@truss-harness/runtime";
4
+ export type AgentMode = "chat" | "plan" | "edit";
5
+ export interface ClientRuntimeOptions {
6
+ readonly workspaceRoot: string;
7
+ readonly provider: LocalEndpointKind;
8
+ readonly baseUrl: string;
9
+ readonly model: string;
10
+ readonly apiKey?: string;
11
+ readonly systemPrompt?: string;
12
+ readonly approval?: ToolApproval;
13
+ readonly mode?: AgentMode;
14
+ readonly internetAccess?: boolean;
15
+ readonly mcpServers?: McpServerConfigurations;
16
+ }
17
+ export interface ClientRuntime {
18
+ readonly runtime: AgentRuntime;
19
+ readonly events: EventBus<RuntimeEvent>;
20
+ readonly mcpServers: readonly McpServerStatus[];
21
+ dispose(): Promise<void>;
22
+ }
23
+ export declare function createClientRuntime(options: ClientRuntimeOptions): Promise<ClientRuntime>;
24
+ export interface ClientConfiguration extends ClientRuntimeOptions {
25
+ }
26
+ export declare function configurationFromEnvironment(workspaceRoot: string, environment?: NodeJS.ProcessEnv): ClientConfiguration;
@@ -0,0 +1,79 @@
1
+ import { createLocalModelProvider } from "@truss-harness/provider-openai-compatible";
2
+ import { parseMcpServerConfigurations, registerMcpServers } from "@truss-harness/mcp";
3
+ import { AgentRuntime, CompositeContextManager, EventBus, FileWorkspaceMemoryStore, InMemorySessionStore, ToolRegistry, WorkspaceMemoryContextProvider, WorkspacePlanContextProvider, FileWorkspacePlanStore, createUpdatePlanTool, grepTool, listDirectoryTool, readFileTool, registerCoreTools, registerWebTools, searchFilesTool } from "@truss-harness/runtime";
4
+ export async function createClientRuntime(options) {
5
+ const events = new EventBus();
6
+ const tools = new ToolRegistry();
7
+ const memory = new FileWorkspaceMemoryStore(options.workspaceRoot);
8
+ const plans = new FileWorkspacePlanStore(options.workspaceRoot);
9
+ const mode = options.mode ?? "chat";
10
+ if (mode === "edit") {
11
+ registerCoreTools(tools);
12
+ tools.register(createUpdatePlanTool(plans));
13
+ }
14
+ if (mode === "plan") {
15
+ tools.register(readFileTool);
16
+ tools.register(listDirectoryTool);
17
+ tools.register(searchFilesTool);
18
+ tools.register(grepTool);
19
+ }
20
+ if (options.internetAccess)
21
+ registerWebTools(tools);
22
+ const enabledMcpServers = mode === "edit"
23
+ ? options.mcpServers
24
+ : mode === "plan"
25
+ ? Object.fromEntries(Object.entries(options.mcpServers ?? {}).filter(([, server]) => server.readOnly))
26
+ : {};
27
+ const mcp = await registerMcpServers(tools, enabledMcpServers, { workspaceRoot: options.workspaceRoot });
28
+ return {
29
+ events,
30
+ mcpServers: mcp.statuses,
31
+ dispose: () => mcp.close(),
32
+ runtime: new AgentRuntime({
33
+ provider: createLocalModelProvider({
34
+ kind: options.provider,
35
+ baseUrl: options.baseUrl,
36
+ model: options.model,
37
+ apiKey: options.apiKey
38
+ }),
39
+ tools,
40
+ sessions: new InMemorySessionStore(),
41
+ context: new CompositeContextManager([new WorkspacePlanContextProvider(plans), new WorkspaceMemoryContextProvider(memory)]),
42
+ events,
43
+ workspaceRoot: options.workspaceRoot,
44
+ systemPrompt: [
45
+ options.systemPrompt,
46
+ mode === "plan" ? "You are in Plan mode. Inspect the workspace with read-only tools as needed, then finish with a concise Markdown checklist exactly in this form: a heading '# Plan: <title>' followed by 3 to 8 actionable '- [ ] <step>' lines. Do not make changes." : undefined,
47
+ mode === "edit" ? "You have no direct filesystem access: tools are the only way to inspect or change the workspace. For a request to modify a file, use read_file as needed and then a successful write_file or replace_in_file call. The terminal is for builds, tests, Git, and inspection; never use shell redirection, echo, PowerShell content commands, or any terminal command to write source files. Never simulate tool calls, invent file contents, or claim that a file was created, changed, or verified unless that write tool completed successfully during this run. After a successful file write, read the file to verify it and then finish the task; do not write the same file again unless that verification shows a further focused change is required. If no write succeeds, say plainly that no file was changed and state why. When an active plan is present in context, use update_plan to mark a step in_progress before work and completed after verifying it. Keep the checklist accurate." : undefined
48
+ ].filter(Boolean).join("\n\n"),
49
+ approval: options.approval,
50
+ memory,
51
+ plans,
52
+ savePlanOnCompletion: mode === "plan"
53
+ })
54
+ };
55
+ }
56
+ export function configurationFromEnvironment(workspaceRoot, environment = process.env) {
57
+ const provider = environment.TRUSS_HARNESS_PROVIDER === "openai-compatible" ? "openai-compatible" : "ollama";
58
+ const mode = environment.TRUSS_HARNESS_AGENT_MODE === "edit" || environment.TRUSS_HARNESS_AGENT_MODE === "plan"
59
+ ? environment.TRUSS_HARNESS_AGENT_MODE
60
+ : "chat";
61
+ const baseUrl = environment.TRUSS_HARNESS_BASE_URL ?? (provider === "ollama" ? "http://localhost:11434" : "http://localhost:1234/v1");
62
+ const model = environment.TRUSS_HARNESS_MODEL;
63
+ if (!model) {
64
+ throw new Error("Set TRUSS_HARNESS_MODEL to the model name exposed by your OpenAI-compatible server.");
65
+ }
66
+ return {
67
+ workspaceRoot,
68
+ provider,
69
+ baseUrl,
70
+ model,
71
+ apiKey: environment.TRUSS_HARNESS_API_KEY,
72
+ systemPrompt: environment.TRUSS_HARNESS_SYSTEM_PROMPT,
73
+ mode,
74
+ internetAccess: environment.TRUSS_HARNESS_INTERNET_ACCESS === "true" || environment.TRUSS_HARNESS_INTERNET_ACCESS === "1",
75
+ mcpServers: environment.TRUSS_HARNESS_MCP_SERVERS
76
+ ? parseMcpServerConfigurations(JSON.parse(environment.TRUSS_HARNESS_MCP_SERVERS))
77
+ : undefined
78
+ };
79
+ }
package/logo.png ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@truss-harness/cli",
3
+ "version": "0.1.0",
4
+ "description": "Command-line client and runtime service for Truss.",
5
+ "type": "module",
6
+ "exports": {
7
+ "./runtime": {
8
+ "types": "./dist/runtime.d.ts",
9
+ "default": "./dist/runtime.js"
10
+ },
11
+ "./config": {
12
+ "types": "./dist/config.d.ts",
13
+ "default": "./dist/config.js"
14
+ }
15
+ },
16
+ "bin": {
17
+ "truss-cli": "./dist/bin.js"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc -b",
21
+ "postinstall": "node scripts/postinstall.mjs",
22
+ "prepack": "node ../../scripts/clean-publish-tests.mjs",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "scripts/postinstall.mjs",
28
+ "README.md",
29
+ "logo.png"
30
+ ],
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/truss-agent/truss-harness.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/truss-agent/truss-harness/issues"
41
+ },
42
+ "homepage": "https://github.com/truss-agent/truss-harness#readme",
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "keywords": [
47
+ "ai",
48
+ "coding-agent",
49
+ "cli",
50
+ "ollama",
51
+ "lm-studio",
52
+ "local-first"
53
+ ],
54
+ "dependencies": {
55
+ "@truss-harness/branding": "0.1.0",
56
+ "@truss-harness/mcp": "0.1.0",
57
+ "@truss-harness/provider-openai-compatible": "0.1.0",
58
+ "@truss-harness/runtime": "0.1.0"
59
+ }
60
+ }
@@ -0,0 +1,12 @@
1
+ if (process.env.npm_config_global === "true" || process.env.npm_config_global === "1") {
2
+ process.stdout.write(`
3
+ Truss CLI installed.
4
+
5
+ Next steps:
6
+ truss-cli models
7
+ truss-cli config init
8
+ truss-cli chat "Explain this workspace"
9
+
10
+ Run truss-cli help for modes, permissions, configuration, and examples.
11
+ `);
12
+ }