@bastani/atomic 0.5.5 → 0.5.6-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.
Files changed (37) hide show
  1. package/README.md +60 -34
  2. package/dist/sdk/components/compact-switcher.d.ts +10 -0
  3. package/dist/sdk/components/compact-switcher.d.ts.map +1 -0
  4. package/dist/sdk/components/orchestrator-panel-store.d.ts +21 -1
  5. package/dist/sdk/components/orchestrator-panel-store.d.ts.map +1 -1
  6. package/dist/sdk/components/orchestrator-panel-types.d.ts +1 -0
  7. package/dist/sdk/components/orchestrator-panel-types.d.ts.map +1 -1
  8. package/dist/sdk/components/session-graph-panel.d.ts.map +1 -1
  9. package/dist/sdk/components/statusline.d.ts.map +1 -1
  10. package/dist/sdk/runtime/executor.d.ts +3 -2
  11. package/dist/sdk/runtime/executor.d.ts.map +1 -1
  12. package/dist/sdk/runtime/tmux.d.ts +82 -2
  13. package/dist/sdk/runtime/tmux.d.ts.map +1 -1
  14. package/dist/sdk/workflows/index.d.ts +2 -2
  15. package/dist/sdk/workflows/index.d.ts.map +1 -1
  16. package/package.json +1 -1
  17. package/src/cli.ts +150 -27
  18. package/src/commands/cli/chat/index.ts +25 -14
  19. package/src/commands/cli/completions.ts +24 -0
  20. package/src/commands/cli/session.test.ts +491 -0
  21. package/src/commands/cli/session.ts +265 -0
  22. package/src/commands/cli/workflow.ts +1 -1
  23. package/src/completions/bash.ts +107 -0
  24. package/src/completions/fish.ts +126 -0
  25. package/src/completions/index.ts +7 -0
  26. package/src/completions/powershell.ts +184 -0
  27. package/src/completions/zsh.ts +144 -0
  28. package/src/sdk/components/compact-switcher.tsx +73 -0
  29. package/src/sdk/components/orchestrator-panel-store.test.ts +124 -0
  30. package/src/sdk/components/orchestrator-panel-store.ts +36 -1
  31. package/src/sdk/components/orchestrator-panel-types.ts +2 -0
  32. package/src/sdk/components/session-graph-panel.tsx +138 -10
  33. package/src/sdk/components/statusline.tsx +13 -8
  34. package/src/sdk/runtime/executor.ts +18 -27
  35. package/src/sdk/runtime/tmux.conf +18 -0
  36. package/src/sdk/runtime/tmux.ts +198 -24
  37. package/src/sdk/workflows/index.ts +7 -1
package/src/cli.ts CHANGED
@@ -5,20 +5,86 @@
5
5
  * Built with Commander.js for robust argument parsing and type-safe options.
6
6
  *
7
7
  * Usage:
8
- * atomic Interactive setup (same as 'atomic init')
9
- * atomic init Interactive setup with agent selection
10
- * atomic init -a <agent> Setup specific agent (skip selection)
11
- * atomic init -s <scm> Setup specific SCM (github, sapling)
12
- * atomic chat -a <agent> Start interactive chat with an agent
13
- * atomic config set <key> <value> Set configuration value
14
- * atomic --version Show version
15
- * atomic --help Show help
8
+ * atomic Interactive setup (same as 'atomic init')
9
+ * atomic init Interactive setup with agent selection
10
+ * atomic init -a <agent> Setup specific agent (skip selection)
11
+ * atomic init -s <scm> Setup specific SCM (github, sapling)
12
+ * atomic chat -a <agent> Start interactive chat with an agent
13
+ * atomic chat session list List running chat/workflow sessions
14
+ * atomic chat session connect <id> Attach to a session
15
+ * atomic workflow list List available workflows
16
+ * atomic workflow session list List running sessions
17
+ * atomic workflow session connect <id> Attach to a session
18
+ * atomic session list List all running sessions
19
+ * atomic session connect [id] Interactive session picker
20
+ * atomic config set <key> <value> Set configuration value
21
+ * atomic --version Show version
22
+ * atomic --help Show help
16
23
  */
17
24
 
18
25
  import { Command } from "@commander-js/extra-typings";
19
26
  import { VERSION } from "./version.ts";
20
27
  import { COLORS } from "./theme/colors.ts";
21
28
  import { AGENT_CONFIG, type AgentKey, SCM_CONFIG, type SourceControlType } from "./services/config/index.ts";
29
+ import { SUPPORTED_SHELLS, type Shell } from "./completions/index.ts";
30
+
31
+ // ─── Session subcommand factory ─────────────────────────────────────────────
32
+
33
+ /**
34
+ * Build a `session` subcommand group with `list` and `connect` children.
35
+ * Reused under `chat`, `workflow`, and at the top level.
36
+ */
37
+ /** Commander collect helper: accumulates repeated `-a` values into an array. */
38
+ function collectAgent(value: string, previous: string[]): string[] {
39
+ return [...previous, value];
40
+ }
41
+
42
+ function addSessionSubcommand(parent: Command, scope: "chat" | "workflow" | "all" = "all") {
43
+ const sessionCmd = parent
44
+ .command("session")
45
+ .description("Manage running tmux sessions");
46
+
47
+ sessionCmd
48
+ .command("list")
49
+ .description("List running sessions on the atomic tmux socket")
50
+ .option(
51
+ "-a, --agent <name>",
52
+ `Filter by agent backend (${Object.keys(AGENT_CONFIG).join(", ")}); repeatable`,
53
+ collectAgent,
54
+ [] as string[],
55
+ )
56
+ .action(async (localOpts) => {
57
+ const { sessionListCommand } = await import("./commands/cli/session.ts");
58
+ const exitCode = await sessionListCommand(localOpts.agent, scope);
59
+ process.exit(exitCode);
60
+ });
61
+
62
+ sessionCmd
63
+ .command("connect")
64
+ .description("Attach to a running session (interactive picker when no id given)")
65
+ .argument("[session_id]", "Session name to connect to")
66
+ .option(
67
+ "-a, --agent <name>",
68
+ `Filter picker by agent backend (${Object.keys(AGENT_CONFIG).join(", ")}); repeatable`,
69
+ collectAgent,
70
+ [] as string[],
71
+ )
72
+ .action(async (sessionId, localOpts) => {
73
+ if (sessionId) {
74
+ const { sessionConnectCommand } = await import("./commands/cli/session.ts");
75
+ const exitCode = await sessionConnectCommand(sessionId);
76
+ process.exit(exitCode);
77
+ } else {
78
+ const { sessionPickerCommand } = await import("./commands/cli/session.ts");
79
+ const exitCode = await sessionPickerCommand(localOpts.agent, scope);
80
+ process.exit(exitCode);
81
+ }
82
+ });
83
+
84
+ return sessionCmd;
85
+ }
86
+
87
+ // ─── Program ────────────────────────────────────────────────────────────────
22
88
 
23
89
  /**
24
90
  * Create and configure the main CLI program
@@ -28,6 +94,9 @@ export function createProgram() {
28
94
  .name("atomic")
29
95
  .description("Configuration management CLI for coding agents")
30
96
  .version(VERSION, "-v, --version", "Show version number")
97
+ // Required so subcommands (workflow list, session connect) can define
98
+ // their own options without the parent absorbing them first.
99
+ .enablePositionalOptions()
31
100
 
32
101
  // Global options available to all commands
33
102
  .option("-y, --yes", "Auto-confirm all prompts (non-interactive mode)")
@@ -71,13 +140,15 @@ export function createProgram() {
71
140
  });
72
141
  });
73
142
 
74
- // Add chat command (default command when no subcommand is provided)
75
- program
143
+ // ── Chat command (default) ──────────────────────────────────────────────
144
+ const chatCmd = program
76
145
  .command("chat", { isDefault: true })
77
146
  .description("Start an interactive chat session with a coding agent")
78
147
  .option("-a, --agent <name>", `Agent to chat with (${agentChoices})`)
79
148
  .allowUnknownOption()
80
149
  .allowExcessArguments(true)
150
+ .enablePositionalOptions()
151
+ .passThroughOptions()
81
152
  .addHelpText(
82
153
  "after",
83
154
  `
@@ -88,8 +159,8 @@ Examples:
88
159
  $ atomic chat -a copilot Start Copilot interactively
89
160
  $ atomic chat -a opencode Start OpenCode interactively
90
161
  $ atomic chat -a claude "fix the bug" Claude with initial prompt
91
- $ atomic chat -a copilot --model gpt-4o Copilot with custom model
92
- $ atomic chat -a claude --verbose Forward --verbose to claude`,
162
+ $ atomic chat session list List running sessions
163
+ $ atomic chat session connect <id> Attach to a session`,
93
164
  )
94
165
  .action(async (localOpts, cmd) => {
95
166
  const validAgents = Object.keys(AGENT_CONFIG);
@@ -126,48 +197,72 @@ Examples:
126
197
  process.exit(exitCode);
127
198
  });
128
199
 
129
- // Add workflow command
200
+ // Chat session subcommands: atomic chat session list / connect
201
+ addSessionSubcommand(chatCmd, "chat");
202
+
203
+ // ── Workflow command ─────────────────────────────────────────────────────
130
204
  //
131
- // Two shapes are supported behind a single command:
205
+ // Three shapes:
132
206
  // 1. `atomic workflow -a <agent>` — interactive picker
133
- // 2. `atomic workflow -n <name> -a <agent> ...` — named run with
134
- // either a positional prompt (free-form workflows) or
135
- // `--<field>=<value>` flags (structured-input workflows).
207
+ // 2. `atomic workflow -n <name> -a <agent> ...` — named run
208
+ // 3. `atomic workflow list [-a <agent>]` — list workflows
136
209
  //
137
- // `allowUnknownOption` + `allowExcessArguments` give us both: unknown
138
- // flags and positional tokens land in `cmd.args`, which we forward
139
- // as `passthroughArgs` so the command layer can parse them against
140
- // the workflow's declared schema.
141
- program
210
+ // `allowUnknownOption` + `allowExcessArguments` let unknown flags and
211
+ // positional tokens land in `cmd.args`, forwarded as `passthroughArgs`
212
+ // so the command layer can parse them against the workflow's schema.
213
+ const workflowCmd = program
142
214
  .command("workflow")
143
215
  .description("Run a multi-session agent workflow")
144
216
  .option("-n, --name <name>", "Workflow name (matches directory under .atomic/workflows/<name>/)")
145
217
  .option("-a, --agent <name>", `Agent to use (${agentChoices})`)
146
- .option("-l, --list", "List available workflows")
147
218
  .allowUnknownOption()
148
219
  .allowExcessArguments(true)
220
+ .enablePositionalOptions()
221
+ .passThroughOptions()
149
222
  .addHelpText(
150
223
  "after",
151
224
  `
152
225
  Examples:
153
- $ atomic workflow -l List available workflows
226
+ $ atomic workflow list List available workflows
227
+ $ atomic workflow list -a claude List Claude workflows only
154
228
  $ atomic workflow -a claude Open the interactive picker
155
229
  $ atomic workflow -n ralph -a claude "fix bug" Run a free-form workflow
156
230
  $ atomic workflow -n gen-spec -a claude --research_doc=notes.md --focus=standard
157
- Run a structured-input workflow`,
231
+ Run a structured-input workflow
232
+ $ atomic workflow session list List running sessions
233
+ $ atomic workflow session connect <id> Attach to a session`,
158
234
  )
159
235
  .action(async (localOpts, cmd) => {
160
236
  const { workflowCommand } = await import("./commands/cli/workflow.ts");
161
237
  const exitCode = await workflowCommand({
162
238
  name: localOpts.name,
163
239
  agent: localOpts.agent,
164
- list: localOpts.list,
165
240
  passthroughArgs: cmd.args,
166
241
  });
167
242
  process.exit(exitCode);
168
243
  });
169
244
 
170
- // Add config command for managing CLI settings
245
+ // Workflow list subcommand: atomic workflow list [-a <agent>]
246
+ workflowCmd
247
+ .command("list")
248
+ .description("List available workflows")
249
+ .option("-a, --agent <name>", `Filter by agent (${agentChoices})`)
250
+ .action(async (localOpts) => {
251
+ const { workflowCommand } = await import("./commands/cli/workflow.ts");
252
+ const exitCode = await workflowCommand({
253
+ list: true,
254
+ agent: localOpts.agent,
255
+ });
256
+ process.exit(exitCode);
257
+ });
258
+
259
+ // Workflow session subcommands: atomic workflow session list / connect
260
+ addSessionSubcommand(workflowCmd, "workflow");
261
+
262
+ // ── Top-level session command ───────────────────────────────────────────
263
+ addSessionSubcommand(program);
264
+
265
+ // ── Config command ──────────────────────────────────────────────────────
171
266
  const configCmd = program
172
267
  .command("config")
173
268
  .description("Manage atomic configuration");
@@ -184,6 +279,34 @@ Examples:
184
279
  process.exit(exitCode);
185
280
  });
186
281
 
282
+ // ── Completions command ────────────────────────────────────────────────
283
+ program
284
+ .command("completions")
285
+ .description("Output shell completion script")
286
+ .argument("<shell>", `Shell type (${SUPPORTED_SHELLS.join(", ")})`)
287
+ .addHelpText(
288
+ "after",
289
+ `
290
+ Install completions for your shell:
291
+
292
+ Bash eval "$(atomic completions bash)" # add to ~/.bashrc
293
+ Zsh eval "$(atomic completions zsh)" # add to ~/.zshrc
294
+ Fish atomic completions fish | source # or save to ~/.config/fish/completions/atomic.fish
295
+ PowerShell atomic completions powershell | Invoke-Expression # add to $PROFILE`,
296
+ )
297
+ .action(async (shell) => {
298
+ if (!SUPPORTED_SHELLS.includes(shell as Shell)) {
299
+ console.error(
300
+ `${COLORS.red}Error: Unknown shell '${shell}'${COLORS.reset}`,
301
+ );
302
+ console.error(`Supported shells: ${SUPPORTED_SHELLS.join(", ")}`);
303
+ process.exit(1);
304
+ }
305
+ const { completionsCommand } = await import("./commands/cli/completions.ts");
306
+ const exitCode = completionsCommand(shell as Shell);
307
+ process.exit(exitCode);
308
+ });
309
+
187
310
  return program;
188
311
  }
189
312
 
@@ -24,15 +24,18 @@ import {
24
24
  } from "../../../services/config/atomic-global-config.ts";
25
25
  import { getConfigRoot } from "../../../services/config/config-path.ts";
26
26
  import {
27
+ isInsideAtomicSocket,
27
28
  isInsideTmux,
28
29
  isTmuxInstalled,
29
30
  resetMuxBinaryCache,
30
31
  } from "../../../sdk/workflows/index.ts";
31
32
  import {
32
33
  createSession,
34
+ detachAndAttachAtomic,
33
35
  killSession,
36
+ setSessionEnv,
34
37
  spawnMuxAttach,
35
- SOCKET_NAME,
38
+ switchClient,
36
39
  } from "../../../sdk/workflows/index.ts";
37
40
  import { ensureTmuxInstalled } from "../../../lib/spawn.ts";
38
41
 
@@ -137,11 +140,10 @@ function buildLauncherScript(
137
140
  /**
138
141
  * Spawn the native agent CLI as an interactive subprocess.
139
142
  *
140
- * When running inside a tmux/psmux session, the agent spawns inline
141
- * in the current pane with inherited stdio.
142
- *
143
- * When running outside tmux, a new tmux session is created and
144
- * attached so the agent benefits from multiplexer features.
143
+ * Always creates a new session in the atomic tmux socket and attaches
144
+ * to it, regardless of whether the user is already inside tmux.
145
+ * Falls back to direct spawn only when no TTY is available or tmux
146
+ * cannot be installed.
145
147
  *
146
148
  * @param options - Chat command configuration options
147
149
  * @returns Exit code from the agent process
@@ -175,11 +177,6 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
175
177
  const cmd = [config.cmd, ...args];
176
178
  const envVars = config.env_vars;
177
179
 
178
- // ── Inside tmux: spawn inline in the current pane ──
179
- if (isInsideTmux()) {
180
- return spawnDirect(cmd, projectRoot, envVars);
181
- }
182
-
183
180
  // ── No TTY: tmux attach requires a real terminal ──
184
181
  if (!process.stdin.isTTY) {
185
182
  return spawnDirect(cmd, projectRoot, envVars);
@@ -202,7 +199,7 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
202
199
 
203
200
  // ── Build launcher script for safe arg/cwd handling ──
204
201
  const chatId = generateChatId();
205
- const windowName = `atomic-chat-${chatId}`;
202
+ const windowName = `atomic-chat-${agentType}-${chatId}`;
206
203
 
207
204
  const sessionsDir = join(homedir(), ".atomic", "sessions", "chat");
208
205
  await mkdir(sessionsDir, { recursive: true });
@@ -219,11 +216,25 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
219
216
  ? `pwsh -NoProfile -File "${launcherPath}"`
220
217
  : `bash "${launcherPath}"`;
221
218
 
222
- // ── Outside tmux: create a new session and attach ──
219
+ // ── Create session on the atomic socket and attach ──
223
220
  try {
224
221
  createSession(windowName, shellCmd, undefined, projectRoot);
222
+ setSessionEnv(windowName, "ATOMIC_AGENT", agentType);
225
223
 
226
- console.log(`[atomic] Session: ${windowName} (FYI all atomic sessions run on tmux -L ${SOCKET_NAME})`);
224
+ if (isInsideAtomicSocket()) {
225
+ // Already on the atomic server — just switch to the new session.
226
+ switchClient(windowName);
227
+ try { await rm(launcherPath, { force: true }); } catch {}
228
+ return 0;
229
+ }
230
+
231
+ if (isInsideTmux()) {
232
+ // Inside a different tmux server — detach and replace the client
233
+ // with an attach to the atomic socket (no nesting).
234
+ detachAndAttachAtomic(windowName);
235
+ try { await rm(launcherPath, { force: true }); } catch {}
236
+ return 0;
237
+ }
227
238
 
228
239
  const attachProc = spawnMuxAttach(windowName);
229
240
  const exitCode = await attachProc.exited;
@@ -0,0 +1,24 @@
1
+ import {
2
+ bashCompletionScript,
3
+ zshCompletionScript,
4
+ fishCompletionScript,
5
+ powershellCompletionScript,
6
+ type Shell,
7
+ } from "../../completions/index.ts";
8
+
9
+ const SCRIPTS: Record<Shell, string> = {
10
+ bash: bashCompletionScript,
11
+ zsh: zshCompletionScript,
12
+ fish: fishCompletionScript,
13
+ powershell: powershellCompletionScript,
14
+ };
15
+
16
+ /**
17
+ * Print the shell completion script for the given shell to stdout.
18
+ * Returns 0 on success, 1 on unknown shell.
19
+ */
20
+ export function completionsCommand(shell: Shell): number {
21
+ const script = SCRIPTS[shell];
22
+ process.stdout.write(script);
23
+ return 0;
24
+ }