@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 +21 -0
- package/README.md +93 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +174 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.js +256 -0
- package/dist/protocol.d.ts +50 -0
- package/dist/protocol.js +101 -0
- package/dist/runtime.d.ts +26 -0
- package/dist/runtime.js +79 -0
- package/logo.png +0 -0
- package/package.json +60 -0
- package/scripts/postinstall.mjs +12 -0
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
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
|
+
});
|
package/dist/config.d.ts
ADDED
|
@@ -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>;
|
package/dist/protocol.js
ADDED
|
@@ -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;
|
package/dist/runtime.js
ADDED
|
@@ -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
|
+
}
|