@standardagents/code 0.0.0-dev.fffff

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,139 @@
1
+ <div align="center">
2
+
3
+ ```
4
+ █████████████
5
+ ███ ██
6
+ ██ █
7
+ ████ ████████ ██
8
+ ███ ████████ ███
9
+ ██ ████████ ████
10
+ █ ██
11
+ ██ ███
12
+ █████████████
13
+ ```
14
+
15
+ # Standard Code
16
+
17
+ **A best-in-class terminal coding agent.**
18
+
19
+ The LLM loop runs remotely on a [Standard Agents](https://github.com/standardagents) instance;
20
+ every filesystem and shell tool executes locally on your machine via this CLI — so the agent
21
+ works in *your* environment, against *your* code, with *your* permissions.
22
+
23
+ ```
24
+ ┌─────────────────────────┐ bridge (WebSocket) ┌──────────────────────┐
25
+ │ Standard Agents instance│ ── tool_request ──────────────▶ │ Standard Code CLI │
26
+ │ (the agent's LLM loop) │ ◀──────────── tool_response ── │ (your machine) │
27
+ └─────────────────────────┘ └──────────────────────┘
28
+ ```
29
+
30
+ </div>
31
+
32
+ ---
33
+
34
+ ## 🔒 Early Access
35
+
36
+ Standard Code is in **early access**. The agent loop, tooling, and backend are evolving fast,
37
+ and we're rolling it out gradually to keep the feedback loop tight.
38
+
39
+ **Access is invite-only.** To run Standard Code you need:
40
+
41
+ 1. A spot on the **early-access list** — request one at
42
+ **[standardagents.ai/early-access](https://standardagents.ai/early-access)**.
43
+ 2. A Standard Agents instance exposing the `standard_code_agent`
44
+ (see [agentbuilder](https://github.com/standardagents/agentbuilder)).
45
+
46
+ If you're already on the list, you'll have received an instance URL and an API token. Jump to
47
+ [Quick start](#quick-start). If not, [join the waitlist](https://standardagents.ai/early-access) —
48
+ we're letting new folks in every week.
49
+
50
+ ---
51
+
52
+ ## Why Standard Code
53
+
54
+ - **Your machine, your rules.** Tools run on your host — read/write/edit files, list, grep, glob,
55
+ bash, delete — never in some sandboxed cloud container. The agent edits the same files you do.
56
+ - **Permission-aware by design.** A single auto-accept level (1–5) controls what runs unattended;
57
+ anything riskier prompts you first. A catastrophic-command guard blocks dangerous shell calls
58
+ at *every* level, so you can't accidentally `rm -rf` your way into a bad day.
59
+ - **Durable, resumable sessions.** Long-running and permission-gated calls *park* and resume over
60
+ HTTP — a slow approval or a dropped socket can't kill your turn. Resume the same session from
61
+ any machine.
62
+ - **Background processes that survive.** Dev servers, watchers, queue workers are tracked in the
63
+ thread and keep running across reconnects and resumes.
64
+ - **Subagents & research built in.** Delegate focused subtasks to a coding subagent, or spin up a
65
+ non-blocking web-research agent that returns source-backed findings.
66
+ - **MCP, first-class.** The CLI is a Model Context Protocol host — connect local MCP servers
67
+ (Playwright, databases, issue trackers, anything) and the agent can use them. Install new ones
68
+ in natural language: *"install the playwright mcp server"* and it researches and wires them up.
69
+
70
+ ---
71
+
72
+ ## Quick start
73
+
74
+ Standard Code is a zero-dependency CLI (Node built-ins only) run through `tsx`:
75
+
76
+ ```bash
77
+ pnpm install # installs tsx + typescript
78
+ pnpm code <dir> # run against a project directory (defaults to cwd)
79
+ # or
80
+ node bin/standardcode.mjs <dir>
81
+ ```
82
+
83
+ On first run it asks for your Standard Agents instance URL and an API token (stored in
84
+ `~/.standardagents/credentials`). It then offers to **resume** a session tagged for this
85
+ project + machine, or **start a new one**.
86
+
87
+ ### Requirements
88
+
89
+ - **Node 22+**
90
+ - A Standard Agents instance exposing the `standard_code_agent`
91
+ (see the [agentbuilder](https://github.com/standardagents/agentbuilder) repo). For local
92
+ development, run that backend on `:5178` and point this CLI at `http://localhost:5178`.
93
+
94
+ ---
95
+
96
+ ## Features
97
+
98
+ - **Forwarded tools** executed on your host: read/write/edit files, list, grep, glob, bash, delete.
99
+ - **Permissions**: a single auto-accept level 1–5 (shift-tab cycles it); risky calls prompt;
100
+ a catastrophic-command guard blocks dangerous shell commands at every level.
101
+ - **Durable, resumable** long-running/permission-gated tool calls (park & resume over HTTP).
102
+ - **Background processes** tracked in the thread (survive resume from any machine).
103
+ - **Subagent delegation** and non-blocking web research.
104
+ - **MCP (Model Context Protocol)**: connect local MCP servers — the CLI is the MCP host,
105
+ negotiating JSON-RPC over stdio. Manage them with `/mcp`; install new ones in natural
106
+ language ("install the playwright mcp server") and the agent researches + installs them.
107
+
108
+ ---
109
+
110
+ ## Tests
111
+
112
+ ```bash
113
+ pnpm test # typecheck + MCP client/protocol + install actuator (no backend needed)
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Architecture & contributing
119
+
120
+ Standard Code is the **client** half of a two-part system. The agent loop and tool definitions
121
+ live on the Standard Agents instance ([agentbuilder](https://github.com/standardagents/agentbuilder));
122
+ this CLI connects to a thread, executes the agent's forwarded tool calls on the host, and renders
123
+ the session.
124
+
125
+ See **[`AGENTS.md`](./AGENTS.md)** for the full source map, bridge protocol, MCP internals,
126
+ test layout, and contributor conventions.
127
+
128
+ - Zero runtime dependencies — Node built-ins only, run via `tsx`. No build step for development.
129
+ - Commit messages use Conventional Commits, scope `cli` (e.g. `feat(cli): …`).
130
+
131
+ ---
132
+
133
+ <div align="center">
134
+
135
+ **[Join the early-access list →](https://standardagents.ai/early-access)**
136
+
137
+ Built by [Standard Agents](https://github.com/standardagents).
138
+
139
+ </div>
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ // Launcher for the Standard Code CLI. Runs the TypeScript entry through tsx so
3
+ // it works without a separate build step during local development.
4
+ import { spawn } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createRequire } from "node:module";
7
+ import path from "node:path";
8
+
9
+ const here = path.dirname(fileURLToPath(import.meta.url));
10
+ const entry = path.resolve(here, "../src/index.ts");
11
+ const require = createRequire(import.meta.url);
12
+
13
+ let tsxBin;
14
+ try {
15
+ // Resolve tsx's CLI entry from wherever it is installed in the workspace.
16
+ tsxBin = require.resolve("tsx/cli");
17
+ } catch {
18
+ console.error("Could not find 'tsx'. Run `pnpm install` in the workspace.");
19
+ process.exit(1);
20
+ }
21
+
22
+ const child = spawn(process.execPath, [tsxBin, entry, ...process.argv.slice(2)], {
23
+ stdio: "inherit",
24
+ });
25
+ child.on("exit", (code) => process.exit(code ?? 0));
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@standardagents/code",
3
+ "version": "0.0.0-dev.fffff",
4
+ "description": "Standard Code — a terminal coding agent whose LLM loop runs on a Standard Agents instance while its tools execute on your machine via this CLI.",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "packageManager": "pnpm@11.9.0",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/standardagents/code.git"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public",
14
+ "registry": "https://registry.npmjs.org/"
15
+ },
16
+ "bin": {
17
+ "standardcode": "./bin/standardcode.mjs"
18
+ },
19
+ "files": [
20
+ "bin",
21
+ "src"
22
+ ],
23
+ "scripts": {
24
+ "start": "tsx src/index.ts",
25
+ "code": "tsx src/index.ts",
26
+ "release": "node scripts/release.mjs",
27
+ "release:next": "node scripts/release.mjs --tag=next",
28
+ "release:dev": "node scripts/release.mjs --tag=dev",
29
+ "typecheck": "tsc --noEmit",
30
+ "test": "pnpm typecheck && tsx scripts/mcp-client-test.ts && tsx scripts/mcp-install-tool-test.ts"
31
+ },
32
+ "devDependencies": {
33
+ "@clack/prompts": "^1.6.0",
34
+ "@types/node": "^22.10.0",
35
+ "tsx": "^4.20.6",
36
+ "typescript": "^5.9.3"
37
+ }
38
+ }
package/src/api.ts ADDED
@@ -0,0 +1,169 @@
1
+ /** Minimal REST client for the Standard Agents instance. */
2
+ import type { ThreadSummary } from "./types.ts";
3
+
4
+ export class ApiClient {
5
+ constructor(
6
+ private endpoint: string,
7
+ private token: string
8
+ ) {}
9
+
10
+ get wsEndpoint(): string {
11
+ return this.endpoint.replace(/^http/, "ws");
12
+ }
13
+
14
+ /** The instance origin (used to build admin-UI links). */
15
+ get origin(): string {
16
+ return this.endpoint;
17
+ }
18
+
19
+ get bearer(): string {
20
+ return this.token;
21
+ }
22
+
23
+ private async json<T>(pathname: string, init?: RequestInit): Promise<T> {
24
+ const res = await fetch(`${this.endpoint}${pathname}`, {
25
+ ...init,
26
+ headers: {
27
+ "Content-Type": "application/json",
28
+ Authorization: `Bearer ${this.token}`,
29
+ ...(init?.headers || {}),
30
+ },
31
+ });
32
+ const text = await res.text();
33
+ if (!res.ok) {
34
+ throw new Error(`${init?.method || "GET"} ${pathname} -> ${res.status}: ${text.slice(0, 300)}`);
35
+ }
36
+ try {
37
+ return JSON.parse(text) as T;
38
+ } catch {
39
+ return text as unknown as T;
40
+ }
41
+ }
42
+
43
+ async verify(): Promise<boolean> {
44
+ try {
45
+ await this.json("/api/auth/me");
46
+ return true;
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+
52
+ /** Agent name → display title map (best effort; for labeling subagents). */
53
+ async listAgents(): Promise<{ name: string; title: string }[]> {
54
+ const res = await this.json<{ agents?: any[] } | any[]>("/api/agents");
55
+ const arr = Array.isArray(res) ? res : res.agents || [];
56
+ return arr
57
+ .filter((a) => a && typeof a.name === "string")
58
+ .map((a) => ({ name: a.name, title: typeof a.title === "string" ? a.title : a.name }));
59
+ }
60
+
61
+ async createThread(agentId: string, tags: string[]): Promise<string> {
62
+ const res = await this.json<{ threadId?: string; id?: string }>("/api/threads", {
63
+ method: "POST",
64
+ body: JSON.stringify({ agent_id: agentId, tags }),
65
+ });
66
+ const id = res.threadId || res.id;
67
+ if (!id) throw new Error("Thread create returned no id");
68
+ return id;
69
+ }
70
+
71
+ /** List threads for an agent, optionally filtering to those carrying all given tags. */
72
+ async listThreads(agentId: string, requireTags: string[]): Promise<ThreadSummary[]> {
73
+ const res = await this.json<{ threads?: any[] } | any[]>(
74
+ `/api/threads?agent_id=${encodeURIComponent(agentId)}&limit=100`
75
+ );
76
+ const arr = Array.isArray(res) ? res : res.threads || [];
77
+ return arr
78
+ .map((t) => ({
79
+ id: t.id,
80
+ tags: Array.isArray(t.tags) ? t.tags : [],
81
+ created_at: t.created_at,
82
+ title: t.title,
83
+ preview: t.preview || t.last_message,
84
+ }))
85
+ .filter((t: ThreadSummary) => requireTags.every((tag) => t.tags.includes(tag)));
86
+ }
87
+
88
+ async sendMessage(threadId: string, content: string): Promise<void> {
89
+ await this.json(`/api/threads/${threadId}/messages`, {
90
+ method: "POST",
91
+ body: JSON.stringify({ role: "user", content }),
92
+ });
93
+ }
94
+
95
+ async getMessages(threadId: string, limit = 50): Promise<any[]> {
96
+ const res = await this.json<{ messages?: any[] } | any[]>(
97
+ `/api/threads/${threadId}/messages?limit=${limit}`
98
+ );
99
+ return Array.isArray(res) ? res : res.messages || [];
100
+ }
101
+
102
+ async getLogs(threadId: string, limit = 100): Promise<any[]> {
103
+ const res = await this.json<{ logs?: any[] } | any[]>(
104
+ `/api/threads/${threadId}/logs?limit=${limit}&order=desc`
105
+ );
106
+ return Array.isArray(res) ? res : res.logs || [];
107
+ }
108
+
109
+ /**
110
+ * Deliver a durable forwarded tool result to the thread, resuming the turn.
111
+ * Retries with backoff — this is the durable delivery path, so it must land
112
+ * even if the connection is briefly flaky after a permission wait.
113
+ */
114
+ async postToolResult(
115
+ threadId: string,
116
+ toolCallId: string,
117
+ ok: boolean,
118
+ result?: string,
119
+ error?: string
120
+ ): Promise<boolean> {
121
+ const body = JSON.stringify({ tool_call_id: toolCallId, ok, result, error });
122
+ for (let attempt = 0; attempt < 6; attempt++) {
123
+ try {
124
+ await this.json(`/api/threads/${threadId}/tool-result`, {
125
+ method: "POST",
126
+ headers: { "Content-Type": "application/json" },
127
+ body,
128
+ });
129
+ return true;
130
+ } catch {
131
+ await new Promise((r) => setTimeout(r, Math.min(500 * 2 ** attempt, 8000)));
132
+ }
133
+ }
134
+ return false;
135
+ }
136
+
137
+ /** Read a value from the thread's durable KV store (null if absent). */
138
+ async kvGet(threadId: string, key: string): Promise<unknown> {
139
+ try {
140
+ const res = await this.json<{ value?: unknown }>(
141
+ `/api/threads/${threadId}/kv?key=${encodeURIComponent(key)}`
142
+ );
143
+ return res?.value ?? null;
144
+ } catch {
145
+ return null; // 404 = key not yet set
146
+ }
147
+ }
148
+
149
+ /** Write a value to the thread's durable KV store. */
150
+ async kvSet(threadId: string, key: string, value: unknown): Promise<void> {
151
+ try {
152
+ await this.json(`/api/threads/${threadId}/kv`, {
153
+ method: "POST",
154
+ headers: { "Content-Type": "application/json" },
155
+ body: JSON.stringify({ key, value }),
156
+ });
157
+ } catch {
158
+ // best effort
159
+ }
160
+ }
161
+
162
+ async stop(threadId: string): Promise<void> {
163
+ try {
164
+ await this.json(`/api/threads/${threadId}/stop`, { method: "POST" });
165
+ } catch {
166
+ // best effort
167
+ }
168
+ }
169
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Session approvals — the permission mode plus the tools and risk levels the
3
+ * user has pre-approved — persisted in the THREAD's KV store (server-side), so
4
+ * they resume with the session and a server hook can show the agent what's
5
+ * already approved. Nothing is stored on the client.
6
+ */
7
+ import type { ApiClient } from "./api.ts";
8
+ import type { PermissionState } from "./permissions.ts";
9
+ import type { Level } from "./types.ts";
10
+
11
+ const KEY = "approvals";
12
+
13
+ export interface ApprovalsKV {
14
+ level?: Level;
15
+ allowTools: string[];
16
+ allowRisk: number[];
17
+ }
18
+
19
+ export async function loadApprovals(api: ApiClient, threadId: string): Promise<ApprovalsKV> {
20
+ const v = await api.kvGet(threadId, KEY);
21
+ if (v && typeof v === "object" && !Array.isArray(v)) {
22
+ const o = v as Record<string, unknown>;
23
+ const n = Number(o.level);
24
+ const level = n >= 1 && n <= 5 ? (n as Level) : undefined;
25
+ return {
26
+ level,
27
+ allowTools: Array.isArray(o.allowTools) ? (o.allowTools as string[]) : [],
28
+ allowRisk: Array.isArray(o.allowRisk) ? (o.allowRisk as number[]) : [],
29
+ };
30
+ }
31
+ return { allowTools: [], allowRisk: [] };
32
+ }
33
+
34
+ /** Persist the current approvals to the thread KV (fire-and-forget). */
35
+ export function saveApprovals(api: ApiClient, threadId: string, perm: PermissionState): void {
36
+ const payload: ApprovalsKV = {
37
+ level: perm.level,
38
+ allowTools: Array.from(perm.alwaysAllow).sort(),
39
+ allowRisk: Array.from(perm.allowRisk).sort((a, b) => a - b),
40
+ };
41
+ void api.kvSet(threadId, KEY, payload);
42
+ }