totopo 1.0.6 → 1.0.7
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 +53 -31
- package/bin/totopo.js +8 -9
- package/dist/commands/advanced.js +4 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<img src=".github/assets/logo.png" alt="totopo" width="100%" />
|
|
4
4
|
|
|
5
|
-
A simple CLI to
|
|
5
|
+
A simple CLI to use AI coding agents safely in your local codebase.
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|

|
|
@@ -12,14 +12,26 @@ A simple CLI to spin up a sandboxed environment for AI coding agents.
|
|
|
12
12
|
|
|
13
13
|
Here's the thing about AI agents: they're probabilistic. They occasionally misinterpret instructions, take unexpected shortcuts, or simply get it wrong. Most of the time they're fine. But "most of the time" isn't a great argument for giving them unrestricted access to your machine, your credentials, and your remote repositories.
|
|
14
14
|
|
|
15
|
-
totopo draws a simple boundary: agents get a full, capable environment to work in — they just can't touch anything outside the project, and they can't reach your remote. That's it. No domain whitelisting, no paranoia, no compromise on what the agent can actually do.
|
|
16
|
-
Reasonable containment for non-deterministic tools. Nothing more, nothing less.
|
|
15
|
+
totopo draws a simple boundary: agents get a full, capable environment to work in — they just can't touch anything outside the project, and they can't reach your remote. That's it. No domain whitelisting, no paranoia, no compromise on what the agent can actually do. Just a reasonable containment for non-deterministic tools.
|
|
17
16
|
|
|
18
17
|
Note: no sandbox substitutes for good judgment. Consider keeping any sensitive secrets or privileged scripts away from your agents.
|
|
19
18
|
|
|
19
|
+
## How totopo Works
|
|
20
|
+
|
|
21
|
+
- Unified, simple DX: run `npx totopo` from anywhere inside a local git repo.
|
|
22
|
+
- Installed once per git repository in a `.totopo/` directory at the repo root.
|
|
23
|
+
- Manages one Docker container per repository.
|
|
24
|
+
|
|
25
|
+
To start a development session, run `npx totopo`, choose `Open session` and confirm the desired scope: the full repo, the current directory, or selected files and folders.
|
|
26
|
+
|
|
27
|
+
### Concurrent Sessions
|
|
28
|
+
totopo uses one container per repository (not per session). This keeps resource usage bounded and makes reconnections fast - you can open as many sessions as you need — they all share the same running container.
|
|
29
|
+
|
|
30
|
+
The tradeoff is that only one scope can be active at a time: if you reopen a session with a different scope, totopo recreates the container to match the new mounts, which would terminate any active sessions connected to the previous container.
|
|
31
|
+
|
|
20
32
|
## Requirements
|
|
21
33
|
|
|
22
|
-
- [Docker](https://www.docker.com/products/docker-desktop/) - used to build and run the
|
|
34
|
+
- [Docker](https://www.docker.com/products/docker-desktop/) - used to build and run the dev container
|
|
23
35
|
- [git](https://git-scm.com/) - safeguard to ensure agents only run in projects with version control in place
|
|
24
36
|
|
|
25
37
|
## Quick Start
|
|
@@ -32,47 +44,57 @@ npx totopo
|
|
|
32
44
|
First-time setup — running `npx totopo` in a fresh repo, selecting a runtime mode, and waiting for the Docker image to build for the first time:
|
|
33
45
|

|
|
34
46
|
|
|
35
|
-
Opening a session when totopo is already initialized is quick. The agent is aware of
|
|
47
|
+
Opening a session when totopo is already initialized is quick. The agent is aware of its scope and sandbox constraints:
|
|
36
48
|

|
|
37
49
|
|
|
38
50
|
## Core features at a glance
|
|
39
51
|
|
|
40
|
-
- **
|
|
52
|
+
- **Docker isolation** — AI agents run in a container with strict filesystem and privilege boundaries
|
|
41
53
|
- **Agents can't reach remote** — push, pull, fetch, and clone are blocked inside the container, preventing agents from accidentally affecting your remote repositories
|
|
42
|
-
- **AI CLIs with persistent sessions** — OpenCode, Claude Code and Codex pre-installed, with conversation history that survives restarts and rebuilds
|
|
43
|
-
- **Host-mirror or
|
|
44
|
-
- **Agents are
|
|
45
|
-
- **Scoped
|
|
46
|
-
|
|
47
|
-
---
|
|
54
|
+
- **AI CLIs with persistent sessions** — OpenCode, Claude Code, and Codex are pre-installed, with conversation history that survives restarts and rebuilds
|
|
55
|
+
- **Host-mirror or full runtime** — either match the container environment to your host, or use a standard dev container with the latest stable tools
|
|
56
|
+
- **Agents are scope-aware** — agents are informed of the mounted files and constraints at session start, so they can factor that into how they work
|
|
57
|
+
- **Scoped access** — expose only the files and directories the agent needs
|
|
48
58
|
|
|
49
59
|
## Features in Detail
|
|
50
60
|
|
|
51
|
-
###
|
|
61
|
+
### Container isolation
|
|
52
62
|
|
|
53
|
-
Every session runs inside a Docker container. Your code is bind-mounted from the host
|
|
63
|
+
Every session runs inside a Docker container. Your code is bind-mounted from the host, so edits are immediately visible in your editor. The container enforces several isolation boundaries:
|
|
54
64
|
|
|
55
65
|
| Control | Implementation |
|
|
56
66
|
| --- | --- |
|
|
57
|
-
| Non-root user | All processes run as `devuser` (uid 1001)
|
|
58
|
-
| Filesystem isolation | Only the
|
|
59
|
-
| Git remote block | `protocol.allow = never` in `/etc/gitconfig` — push, pull, fetch, and clone are all refused
|
|
67
|
+
| Non-root user | All processes run as `devuser` (uid 1001) and cannot modify system-level config |
|
|
68
|
+
| Filesystem isolation | Only the selected project paths are mounted; the rest of the host filesystem is not visible |
|
|
69
|
+
| Git remote block | `protocol.allow = never` in `/etc/gitconfig` — push, pull, fetch, and clone are all refused and require root to override |
|
|
60
70
|
| No host credentials forwarded | Host git credentials are never copied into the container |
|
|
61
|
-
| Secrets never in image | API keys loaded at runtime from `~/.totopo/.env` — never baked into the image, never mounted into the container |
|
|
71
|
+
| Secrets never in image | API keys are loaded at runtime from `~/.totopo/.env` — never baked into the image, never mounted into the container |
|
|
62
72
|
| No privilege escalation | `no-new-privileges:true` prevents any process from gaining elevated permissions |
|
|
63
73
|
|
|
64
74
|
Remote git operations are blocked inside the container. Push from your host terminal instead.
|
|
65
75
|
|
|
66
|
-
###
|
|
76
|
+
### Session scope
|
|
67
77
|
|
|
68
|
-
|
|
78
|
+
When you open a session, totopo asks what part of the repository to mount into the container:
|
|
69
79
|
|
|
70
|
-
|
|
80
|
+
- `Repo root` — the full repository
|
|
81
|
+
- `Current directory` — only the current directory
|
|
82
|
+
- `Selective` — specific files and folders chosen interactively
|
|
83
|
+
|
|
84
|
+
The selected scope is stored on the container and checked on every later `Open session`.
|
|
85
|
+
|
|
86
|
+
- If the requested scope matches the existing container, totopo connects directly to it if running, or resumes it if stopped.
|
|
87
|
+
- If the requested scope is different, totopo recreates the container so the mounted paths match the new scope.
|
|
88
|
+
- Parallel terminals on the same scope are fine. totopo connects with `docker exec`, so any concurrency limits are just the normal limits of sharing one running container.
|
|
89
|
+
|
|
90
|
+
This is an intentional tradeoff: you get predictable resource usage and quick reconnects, but only one active mounted view per repository at a time.
|
|
91
|
+
|
|
92
|
+
In `Current directory` and `Selective` scopes, `.git` is intentionally not mounted. Mounting `.git` would expose the full commit history of every repository file, including files outside the mounted paths, which defeats the point of scoped access. As a result, git is unavailable inside those scoped sessions and the agent operates without repository history. The agent is instructed to surface these limitations at session start.
|
|
71
93
|
|
|
72
94
|
Scoped sessions are well-suited for focused tasks where you want to give the agent a narrow, explicit view of your codebase.
|
|
73
95
|
|
|
74
|
-
Example showcasing agent awareness of scope limitations:
|
|
75
|
-

|
|
76
98
|
|
|
77
99
|
### AI CLIs with persistent sessions
|
|
78
100
|
|
|
@@ -84,20 +106,20 @@ claude # Claude Code (Anthropic)
|
|
|
84
106
|
codex # Codex (OpenAI)
|
|
85
107
|
```
|
|
86
108
|
|
|
87
|
-
Agent session data is
|
|
109
|
+
Agent session data is isolated per repository, so agents do not bleed context between projects. To clear memory, run `npx totopo` and navigate to `Advanced > Clear agent memory`. This stops the container if running and removes the `.totopo/agents/` directory.
|
|
88
110
|
|
|
89
111
|
### Dev container runtime
|
|
90
112
|
|
|
91
113
|
Choose between two modes:
|
|
92
114
|
|
|
93
115
|
- **Host-mirror** — the container runtime matches your host Node.js version and selected tools, keeping the environment consistent with your local setup.
|
|
94
|
-
- **
|
|
116
|
+
- **Full** — a full dev container with the latest stable versions of all tools. Good default if you do not need version parity with your host.
|
|
95
117
|
|
|
96
118
|
Either way, basic dev tools and all three AI CLIs are always included.
|
|
97
119
|
|
|
98
120
|
## What gets created in your project
|
|
99
121
|
|
|
100
|
-
```
|
|
122
|
+
```text
|
|
101
123
|
your-project/
|
|
102
124
|
└── .totopo/
|
|
103
125
|
├── Dockerfile # container image definition
|
|
@@ -105,14 +127,14 @@ your-project/
|
|
|
105
127
|
├── settings.json # runtime mode + selected tools (committed with project)
|
|
106
128
|
├── README.md # .totopo reference
|
|
107
129
|
└── agents/ # agent session data — gitignored, created on first session start
|
|
108
|
-
├── claude/
|
|
109
|
-
├── opencode/
|
|
110
|
-
└── codex/
|
|
130
|
+
├── claude/ # mounted as ~/.claude/
|
|
131
|
+
├── opencode/ # mounted as ~/.config/opencode/ + ~/.local/share/opencode/
|
|
132
|
+
└── codex/ # mounted as ~/.codex/
|
|
111
133
|
|
|
112
134
|
~/.totopo/.env # API keys — global, outside all repos, never mounted into container
|
|
113
135
|
```
|
|
114
136
|
|
|
115
|
-
Agent session history and conversation data are persisted in
|
|
137
|
+
totopo is initialized at the repository root, and `.totopo/` lives there regardless of which directory you later open a session from. Agent session history and conversation data are persisted in `.totopo/agents/` across container rebuilds and restarts. This directory is gitignored so session data stays local to your machine.
|
|
116
138
|
|
|
117
139
|
## Limitations
|
|
118
140
|
|
|
@@ -120,4 +142,4 @@ Agent session history and conversation data are persisted in the `agents` direct
|
|
|
120
142
|
|
|
121
143
|
## Disclaimer
|
|
122
144
|
|
|
123
|
-
MIT licensed and fully open source. Fork it, adapt it, make it yours. Issues are welcome — no promises on response time. Use at your own risk.
|
|
145
|
+
MIT licensed and fully open source. Fork it, adapt it, make it yours. Issues are welcome — no promises on response time. Use at your own risk.
|
package/bin/totopo.js
CHANGED
|
@@ -8,6 +8,13 @@ import { execSync, spawnSync } from "node:child_process";
|
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
9
|
import { basename, dirname } from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { run as advanced } from "../dist/commands/advanced.js";
|
|
12
|
+
import { run as dev } from "../dist/commands/dev.js";
|
|
13
|
+
import { run as doctor } from "../dist/commands/doctor.js";
|
|
14
|
+
import { run as menu } from "../dist/commands/menu.js";
|
|
15
|
+
import { run as onboard } from "../dist/commands/onboard.js";
|
|
16
|
+
import { run as stop } from "../dist/commands/stop.js";
|
|
17
|
+
import { run as syncDockerfile } from "../dist/commands/sync-dockerfile.js";
|
|
11
18
|
|
|
12
19
|
// ─── Guard: inside container ──────────────────────────────────────────────────
|
|
13
20
|
try {
|
|
@@ -50,15 +57,6 @@ if (!existsSync(new URL("../dist/commands/sync-dockerfile.js", import.meta.url))
|
|
|
50
57
|
process.exit(1);
|
|
51
58
|
}
|
|
52
59
|
|
|
53
|
-
// ─── Import compiled commands ─────────────────────────────────────────────────
|
|
54
|
-
const { run: syncDockerfile } = await import("../dist/commands/sync-dockerfile.js");
|
|
55
|
-
const { run: doctor } = await import("../dist/commands/doctor.js");
|
|
56
|
-
const { run: onboard } = await import("../dist/commands/onboard.js");
|
|
57
|
-
const { run: menu } = await import("../dist/commands/menu.js");
|
|
58
|
-
const { run: dev } = await import("../dist/commands/dev.js");
|
|
59
|
-
const { run: stop } = await import("../dist/commands/stop.js");
|
|
60
|
-
const { run: advanced } = await import("../dist/commands/advanced.js");
|
|
61
|
-
|
|
62
60
|
// ─── Onboarding ───────────────────────────────────────────────────────────────
|
|
63
61
|
if (!existsSync(`${repoRoot}/.totopo/Dockerfile`)) {
|
|
64
62
|
const completed = await onboard(packageDir, repoRoot);
|
|
@@ -110,6 +108,7 @@ while (showMenu) {
|
|
|
110
108
|
case "advanced": {
|
|
111
109
|
const result = await advanced(packageDir, projectName, repoRoot);
|
|
112
110
|
if (result === "back") showMenu = true;
|
|
111
|
+
if (result === "rebuild") await dev(packageDir, repoRoot);
|
|
113
112
|
break;
|
|
114
113
|
}
|
|
115
114
|
default:
|
|
@@ -7,6 +7,9 @@ import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { cancel, confirm, isCancel, log, multiselect, outro, select } from "@clack/prompts";
|
|
10
|
+
import { run as runDoctor } from "./doctor.js";
|
|
11
|
+
import { run as runRebuild } from "./rebuild.js";
|
|
12
|
+
import { run as runSettings } from "./settings.js";
|
|
10
13
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
11
14
|
function stopAndRemoveContainer(name) {
|
|
12
15
|
spawnSync("docker", ["stop", name], { stdio: "inherit" });
|
|
@@ -154,10 +157,6 @@ async function resetApiKeys(packageDir) {
|
|
|
154
157
|
}
|
|
155
158
|
// ─── Advanced submenu ─────────────────────────────────────────────────────────
|
|
156
159
|
export async function run(packageDir, projectName, repoRoot) {
|
|
157
|
-
// Dynamic imports to avoid circular deps — same pattern as bin/totopo.js
|
|
158
|
-
const { run: runSettings } = await import("./settings.js");
|
|
159
|
-
const { run: runRebuild } = await import("./rebuild.js");
|
|
160
|
-
const { run: runDoctor } = await import("./doctor.js");
|
|
161
160
|
const totopoDir = join(repoRoot, ".totopo");
|
|
162
161
|
while (true) {
|
|
163
162
|
const action = await select({
|
|
@@ -183,7 +182,7 @@ export async function run(packageDir, projectName, repoRoot) {
|
|
|
183
182
|
break;
|
|
184
183
|
case "rebuild":
|
|
185
184
|
await runRebuild(projectName);
|
|
186
|
-
|
|
185
|
+
return "rebuild";
|
|
187
186
|
case "clear-memory":
|
|
188
187
|
await clearAgentMemory(projectName, totopoDir);
|
|
189
188
|
break;
|