totopo 1.0.8 → 2.0.0-rc-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ A simple CLI to use AI coding agents safely in your local codebase.
10
10
 
11
11
  ## Why totopo?
12
12
 
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.
13
+ Here's the thing about AI agents: they're probabilistic. They occasionally misinterpret instructions 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
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.
16
16
 
@@ -18,21 +18,24 @@ Note: no sandbox substitutes for good judgment. Consider keeping any sensitive s
18
18
 
19
19
  ## How totopo Works
20
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.
21
+ totopo organises work around **projects** — any local directory you register with totopo. The first time you run `npx totopo` in a directory, it walks you through a short setup. Every subsequent run, from anywhere inside that directory tree, totopo resolves the project automatically and shows the project menu:
24
22
 
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.
23
+ - **Open session** choose a scope and jump into an AI coding session
24
+ - **Stop container** — stop the running container
25
+ - **Runtime Mode** — adjust runtime mode and installed tools
26
+ - **Rebuild container** — rebuild the docker image (upon changing runtime mode)
27
+
28
+ All config lives in `~/.totopo/` — nothing is written to your project by default.
26
29
 
27
30
  ### 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.
31
+ totopo uses one Docker container per project, not one per session. You can open as many terminal sessions as you need — they all connect to the same container, keeping resource usage bounded and reconnections fast.
29
32
 
30
33
  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
34
 
32
35
  ## Requirements
33
36
 
34
- - [Docker](https://www.docker.com/products/docker-desktop/) - used to build and run the dev container
35
- - [git](https://git-scm.com/) - safeguard to ensure agents only run in projects with version control in place
37
+ - [Docker](https://www.docker.com/products/docker-desktop/) used to build and run the dev container
38
+ - [Node.js](https://nodejs.org/) required to run `npx totopo`
36
39
 
37
40
  ## Quick Start
38
41
 
@@ -41,11 +44,11 @@ cd your-project
41
44
  npx totopo
42
45
  ```
43
46
 
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:
45
- ![First-time setup](.github/assets/demo-onboarding.gif)
47
+ <!--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:-->
48
+ <!-- ![First-time setup](.github/assets/demo-onboarding.gif) -->
46
49
 
47
- Opening a session when totopo is already initialized is quick. The agent is aware of its scope and sandbox constraints:
48
- ![Quick start](.github/assets/demo-quickstart.gif)
50
+ <!--Opening a session when totopo is already initialized is quick. The agent is aware of its scope and sandbox constraints:-->
51
+ <!-- ![Quick start](.github/assets/demo-quickstart.gif) -->
49
52
 
50
53
  ## Core features at a glance
51
54
 
@@ -75,9 +78,9 @@ Remote git operations are blocked inside the container. Push from your host term
75
78
 
76
79
  ### Session scope
77
80
 
78
- When you open a session, totopo asks what part of the repository to mount into the container:
81
+ When you open a session, totopo asks what part of the project to mount into the container:
79
82
 
80
- - `Repo root` — the full repository
83
+ - `Repo root` — the full project directory
81
84
  - `Current directory` — only the current directory
82
85
  - `Selective` — specific files and folders chosen interactively
83
86
 
@@ -87,14 +90,14 @@ The selected scope is stored on the container and checked on every later `Open s
87
90
  - If the requested scope is different, totopo recreates the container so the mounted paths match the new scope.
88
91
  - 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
92
 
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.
93
+ This is an intentional tradeoff: you get predictable resource usage and quick reconnects, but only one active mounted view per project at a time.
91
94
 
92
95
  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.
93
96
 
94
97
  Scoped sessions are well-suited for focused tasks where you want to give the agent a narrow, explicit view of your codebase.
95
98
 
96
- Example showcasing agent awareness of selective scope limitations:
97
- ![Scoped access](.github/assets/demo-scoped.gif)
99
+ <!-- Example showcasing agent awareness of selective scope limitations:-->
100
+ <!-- ![Scoped access](.github/assets/demo-scoped.gif) -->
98
101
 
99
102
  ### AI CLIs with persistent sessions
100
103
 
@@ -106,7 +109,7 @@ claude # Claude Code (Anthropic)
106
109
  codex # Codex (OpenAI)
107
110
  ```
108
111
 
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.
112
+ Agent session data is isolated per project, so agents do not bleed context between projects. To clear memory, run `npx totopo` and navigate to `Manage totopo > Clear agent memory` and select a project. This stops the container if running and removes the agents directory.
110
113
 
111
114
  ### Dev container runtime
112
115
 
@@ -117,27 +120,51 @@ Choose between two modes:
117
120
 
118
121
  Either way, basic dev tools and all three AI CLIs are always included.
119
122
 
120
- ## What gets created in your project
123
+ ## What gets installed
124
+
125
+ All totopo config lives in `~/.totopo/` on your machine — nothing is written to your project directory unless you opt in.
121
126
 
122
127
  ```text
123
- your-project/
124
- └── .totopo/
125
- ├── Dockerfile # container image definition
126
- ├── post-start.mjs # security checks + readiness summary on every start
127
- ├── settings.json # runtime mode + selected tools (committed with project)
128
- ├── README.md # .totopo reference
129
- └── agents/ # agent session data — gitignored, created on first session start
130
- ├── claude/ # mounted as ~/.claude/
131
- ├── opencode/ # mounted as ~/.config/opencode/ + ~/.local/share/opencode/
132
- └── codex/ # mounted as ~/.codex/
133
-
134
- ~/.totopo/.env # API keys — global, outside all repos, never mounted into container
128
+ ~/.totopo/
129
+ ├── .env # API keys — global, never mounted into container
130
+ └── projects/
131
+ └── <id>/ # stable hash of the project root path
132
+ ├── meta.json # project root, display name, container name
133
+ ├── settings.json # runtime mode + selected tools
134
+ ├── Dockerfile # container image definition
135
+ ├── post-start.mjs # security checks + readiness summary on every start
136
+ └── agents/ # agent session data created on first session start
137
+ ├── claude/ # mounted as ~/.claude/
138
+ ├── opencode/ # mounted as ~/.config/opencode/ + ~/.local/share/opencode/
139
+ └── codex/ # mounted as ~/.codex/
135
140
  ```
136
141
 
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.
142
+ Agent session history and conversation data are persisted in the `agents/` directory across container rebuilds and restarts.
143
+
144
+ ### Shared onboarding (optional)
145
+
146
+ If you want contributors to get a one-click setup experience, add a `totopo.yaml` file at your project root:
147
+
148
+ ```yaml
149
+ # totopo.yaml — project anchor
150
+ #
151
+ # name — shown as: "Welcome to <name>."
152
+ # description — shown as: "<description>"
153
+
154
+ name: my-project
155
+ description: Our AI coding sandbox. Ask @alice for the API keys.
156
+ ```
157
+
158
+ When a new contributor runs `npx totopo`, totopo reads this file to anchor the project root and displays the welcome message before prompting for setup. Without it, totopo will find the git root and suggest it as the project root, so `totopo.yaml` is purely optional.
159
+
160
+ To add a project anchor to an existing local-only totopo project, run `npx totopo` and select `Add project anchor` from the project menu.
138
161
 
139
162
  ## Limitations
140
163
 
164
+ **Rename or move** — moving the project directory breaks identity since totopo uses the absolute path as the project key. Re-run `npx totopo` in the new location to onboard again. Orphaned configs can be cleaned up via `Manage totopo`.
165
+
166
+ **Single machine** — `~/.totopo/` is local. Switching to a new machine requires re-onboarding each project.
167
+
141
168
  **Audio / microphone** — the image includes `sox` (required by Claude Code for voice mode), but audio passthrough from the host depends on your OS. macOS, Linux, and Windows each require different device configuration. If you need voice mode, set up audio passthrough manually for your platform.
142
169
 
143
170
  ## Disclaimer
package/bin/totopo.js CHANGED
@@ -1,23 +1,26 @@
1
1
  #!/usr/bin/env node
2
- // =============================================================================
3
- // bin/totopo.js totopo entry point
2
+ // =========================================================================================================================================
3
+ // bin/totopo.js - totopo entry point
4
4
  // Run this from your project directory (or via npx totopo).
5
- // =============================================================================
5
+ // =========================================================================================================================================
6
6
 
7
7
  import { execSync, spawnSync } from "node:child_process";
8
8
  import { existsSync } from "node:fs";
9
- import { basename, dirname } from "node:path";
9
+ import { dirname } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
+ import { cancel, isCancel, select } from "@clack/prompts";
11
12
  import { run as advanced } from "../dist/commands/advanced.js";
12
13
  import { run as dev } from "../dist/commands/dev.js";
13
14
  import { run as doctor } from "../dist/commands/doctor.js";
14
15
  import { run as menu } from "../dist/commands/menu.js";
15
- import { run as onboard } from "../dist/commands/onboard.js";
16
+ import { addProjectAnchor, run as onboard } from "../dist/commands/onboard.js";
17
+ import { run as rebuild } from "../dist/commands/rebuild.js";
18
+ import { run as settings } from "../dist/commands/settings.js";
16
19
  import { run as stop } from "../dist/commands/stop.js";
17
20
  import { run as syncDockerfile } from "../dist/commands/sync-dockerfile.js";
18
- import { toDockerName } from "../dist/lib/docker-name.js";
21
+ import { listProjectIds, resolveProject } from "../dist/lib/project-identity.js";
19
22
 
20
- // ─── Guard: inside container ──────────────────────────────────────────────────
23
+ // --- Guard: inside container -------------------------------------------------------------------------------------------------------------
21
24
  try {
22
25
  if (execSync("whoami", { encoding: "utf8" }).trim() === "devuser") {
23
26
  console.error("");
@@ -29,26 +32,15 @@ try {
29
32
  process.exit(1);
30
33
  }
31
34
  } catch {
32
- // whoami unavailable not blocking
35
+ // whoami unavailable - not blocking
33
36
  }
34
37
 
35
- // ─── Paths ────────────────────────────────────────────────────────────────────
38
+ // --- Paths -------------------------------------------------------------------------------------------------------------------------------
36
39
  // dirname(dirname(...)) walks up from bin/ to the package root.
37
40
  const packageDir = dirname(dirname(fileURLToPath(import.meta.url)));
41
+ const cwd = process.cwd();
38
42
 
39
- let repoRoot;
40
- try {
41
- repoRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
42
- } catch {
43
- console.error("");
44
- console.error(" No git repository found.");
45
- console.error("");
46
- console.error(" totopo requires a git repository. Run 'git init' first, then re-run totopo.");
47
- console.error("");
48
- process.exit(1);
49
- }
50
-
51
- // ─── Guard: dist/ must exist ─────────────────────────────────────────────────
43
+ // --- Guard: dist/ must exist -------------------------------------------------------------------------------------------------------------
52
44
  if (!existsSync(new URL("../dist/commands/sync-dockerfile.js", import.meta.url))) {
53
45
  console.error("");
54
46
  console.error(" totopo: compiled output not found.");
@@ -58,59 +50,106 @@ if (!existsSync(new URL("../dist/commands/sync-dockerfile.js", import.meta.url))
58
50
  process.exit(1);
59
51
  }
60
52
 
61
- // ─── Onboarding ───────────────────────────────────────────────────────────────
62
- if (!existsSync(`${repoRoot}/.totopo/Dockerfile`)) {
63
- const completed = await onboard(packageDir, repoRoot);
64
- if (!completed) process.exit(0);
53
+ // --- Resolve project from CWD (walk-up through ~/.totopo/projects/) ----------------------------------------------------------------------
54
+ let project = resolveProject(cwd);
55
+
56
+ // --- Onboarding (if not in a registered project) -----------------------------------------------------------------------------------------
57
+ if (!project) {
58
+ // Detect project context: git root or totopo.yaml present?
59
+ let gitRoot = null;
60
+ try {
61
+ gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf8", stdio: "pipe" }).trim();
62
+ } catch {
63
+ // Not in a git repo - that's fine
64
+ }
65
+
66
+ const totopoJsonPath = `${gitRoot ?? cwd}/totopo.yaml`;
67
+ const hasTotopoYaml = existsSync(totopoJsonPath);
68
+
69
+ if (gitRoot !== null || hasTotopoYaml) {
70
+ // Has project context - if other projects already exist, let the user choose first
71
+ if (listProjectIds().length > 0) {
72
+ process.stdout.write("\n");
73
+ const choice = await select({
74
+ message: "What would you like to do?",
75
+ options: [
76
+ { value: "setup", label: "Set up totopo for this directory" },
77
+ { value: "manage", label: "Manage totopo →" },
78
+ ],
79
+ });
80
+ if (isCancel(choice)) {
81
+ cancel();
82
+ process.exit(0);
83
+ }
84
+ if (choice === "manage") {
85
+ await advanced(packageDir);
86
+ process.exit(0);
87
+ }
88
+ }
89
+
90
+ const ctx = await onboard(packageDir, cwd);
91
+ if (!ctx) process.exit(0); // cancelled -> exit cleanly
92
+ project = ctx;
93
+ } else {
94
+ // No project context -> show Manage totopo menu directly
95
+ await advanced(packageDir);
96
+ process.exit(0);
97
+ }
65
98
  }
66
99
 
67
- // ─── Sync Dockerfile with host runtimes ───────────────────────────────────────
68
- await syncDockerfile(packageDir, repoRoot);
100
+ // --- Sync Dockerfile with host runtimes --------------------------------------------------------------------------------------------------
101
+ await syncDockerfile(packageDir, project);
69
102
 
70
- // ─── Doctor (silent pre-check) ────────────────────────────────────────────────
71
- const doctorResult = await doctor(repoRoot, false);
103
+ // --- Doctor (silent pre-check) -----------------------------------------------------------------------------------------------------------
104
+ const doctorResult = await doctor(project.projectDir, false);
72
105
  if (!doctorResult.ok) {
73
106
  console.error(" Fix the issues above and re-run totopo.");
74
107
  console.error("");
75
108
  process.exit(1);
76
109
  }
77
110
 
78
- // ─── Gather state for menu ────────────────────────────────────────────────────
79
- const projectName = basename(repoRoot);
80
- const dockerName = toDockerName(projectName);
111
+ // --- Gather container state for menu -----------------------------------------------------------------------------------------------------
112
+ const { containerName } = project.meta;
81
113
 
82
- const dockerResult = spawnSync("docker", ["ps", "--filter", "name=totopo-managed-", "--format", "{{.Names}}"], {
114
+ const dockerResult = spawnSync("docker", ["ps", "--filter", "name=totopo-", "--format", "{{.Names}}"], {
83
115
  encoding: "utf8",
84
116
  });
85
- const activeCount = dockerResult.stdout ? dockerResult.stdout.trim().split("\n").filter(Boolean).length : 0;
117
+ const activeNames = dockerResult.stdout ? dockerResult.stdout.trim().split("\n").filter(Boolean) : [];
118
+ const activeCount = activeNames.length;
119
+ const projectRunning = activeNames.some((n) => n === containerName);
86
120
 
87
- const projectContainerResult = spawnSync("docker", ["ps", "--filter", `name=${dockerName}`, "--format", "{{.Names}}"], {
88
- encoding: "utf8",
89
- });
90
- const projectRunning = (projectContainerResult.stdout ?? "")
91
- .trim()
92
- .split("\n")
93
- .filter(Boolean)
94
- .some((n) => n === dockerName);
95
-
96
- // ─── Interactive menu loop ────────────────────────────────────────────────────
121
+ // --- Interactive menu loop ---------------------------------------------------------------------------------------------------------------
97
122
  let showMenu = true;
98
123
  while (showMenu) {
99
124
  showMenu = false;
100
125
 
101
- const action = await menu({ projectName, activeCount, projectRunning });
126
+ // Re-evaluated each iteration so menu options stay in sync (e.g. after "Add project anchor")
127
+ const hasTotopoYaml = existsSync(`${project.meta.projectRoot}/totopo.yaml`);
128
+
129
+ const action = await menu({ ctx: project, activeCount, projectRunning, hasTotopoYaml });
102
130
 
103
131
  switch (action) {
104
132
  case "dev":
105
- await dev(packageDir, repoRoot);
133
+ await dev(packageDir, project);
134
+ break;
135
+ case "rebuild":
136
+ await rebuild(project.meta.containerName);
137
+ await dev(packageDir, project);
106
138
  break;
107
139
  case "stop":
108
- await stop(projectName);
140
+ await stop(project.meta.containerName);
141
+ break;
142
+ case "settings":
143
+ await settings(packageDir, project);
144
+ showMenu = true;
145
+ break;
146
+ case "add-anchor":
147
+ await addProjectAnchor(project);
148
+ showMenu = true;
109
149
  break;
110
- case "advanced": {
111
- const result = await advanced(packageDir, projectName, repoRoot);
150
+ case "manage-totopo": {
151
+ const result = await advanced(packageDir);
112
152
  if (result === "back") showMenu = true;
113
- if (result === "rebuild") await dev(packageDir, repoRoot);
114
153
  break;
115
154
  }
116
155
  default: