@bastani/atomic 0.5.4 → 0.5.5-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/README.md +44 -1
- package/dist/lib/path-root-guard.d.ts +4 -0
- package/dist/lib/path-root-guard.d.ts.map +1 -0
- package/dist/sdk/components/color-utils.d.ts +1 -0
- package/dist/sdk/components/color-utils.d.ts.map +1 -0
- package/dist/sdk/components/connectors.d.ts +3 -2
- package/dist/sdk/components/connectors.d.ts.map +1 -0
- package/dist/sdk/components/connectors.test.d.ts +1 -0
- package/dist/sdk/components/connectors.test.d.ts.map +1 -0
- package/dist/sdk/components/edge.d.ts +2 -1
- package/dist/sdk/components/edge.d.ts.map +1 -0
- package/dist/sdk/components/error-boundary.d.ts +1 -0
- package/dist/sdk/components/error-boundary.d.ts.map +1 -0
- package/dist/sdk/components/graph-theme.d.ts +2 -1
- package/dist/sdk/components/graph-theme.d.ts.map +1 -0
- package/dist/sdk/components/header.d.ts +1 -0
- package/dist/sdk/components/header.d.ts.map +1 -0
- package/dist/sdk/components/hooks.d.ts +15 -0
- package/dist/sdk/components/hooks.d.ts.map +1 -0
- package/dist/sdk/components/layout.d.ts +2 -1
- package/dist/sdk/components/layout.d.ts.map +1 -0
- package/dist/sdk/components/layout.test.d.ts +1 -0
- package/dist/sdk/components/layout.test.d.ts.map +1 -0
- package/dist/sdk/components/node-card.d.ts +5 -3
- package/dist/sdk/components/node-card.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel-contexts.d.ts +3 -2
- package/dist/sdk/components/orchestrator-panel-contexts.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel-store.d.ts +2 -1
- package/dist/sdk/components/orchestrator-panel-store.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel-store.test.d.ts +1 -0
- package/dist/sdk/components/orchestrator-panel-store.test.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel-types.d.ts +1 -0
- package/dist/sdk/components/orchestrator-panel-types.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel.d.ts +2 -1
- package/dist/sdk/components/orchestrator-panel.d.ts.map +1 -0
- package/dist/sdk/components/session-graph-panel.d.ts +1 -0
- package/dist/sdk/components/session-graph-panel.d.ts.map +1 -0
- package/dist/sdk/components/status-helpers.d.ts +2 -1
- package/dist/sdk/components/status-helpers.d.ts.map +1 -0
- package/dist/sdk/components/statusline.d.ts +2 -1
- package/dist/sdk/components/statusline.d.ts.map +1 -0
- package/dist/sdk/components/workflow-picker-panel.d.ts +11 -8
- package/dist/sdk/components/workflow-picker-panel.d.ts.map +1 -0
- package/dist/sdk/define-workflow.d.ts +2 -1
- package/dist/sdk/define-workflow.d.ts.map +1 -0
- package/dist/sdk/define-workflow.test.d.ts +1 -0
- package/dist/sdk/define-workflow.test.d.ts.map +1 -0
- package/dist/sdk/errors.d.ts +3 -0
- package/dist/sdk/errors.d.ts.map +1 -0
- package/dist/sdk/errors.test.d.ts +2 -0
- package/dist/sdk/errors.test.d.ts.map +1 -0
- package/dist/sdk/index.d.ts +7 -6
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/providers/claude.d.ts +17 -6
- package/dist/sdk/providers/claude.d.ts.map +1 -0
- package/dist/sdk/providers/copilot.d.ts +2 -5
- package/dist/sdk/providers/copilot.d.ts.map +1 -0
- package/dist/sdk/providers/opencode.d.ts +2 -5
- package/dist/sdk/providers/opencode.d.ts.map +1 -0
- package/dist/sdk/runtime/discovery.d.ts +2 -1
- package/dist/sdk/runtime/discovery.d.ts.map +1 -0
- package/dist/sdk/runtime/executor-entry.d.ts +1 -0
- package/dist/sdk/runtime/executor-entry.d.ts.map +1 -0
- package/dist/sdk/runtime/executor.d.ts +3 -6
- package/dist/sdk/runtime/executor.d.ts.map +1 -0
- package/dist/sdk/runtime/executor.test.d.ts +1 -0
- package/dist/sdk/runtime/executor.test.d.ts.map +1 -0
- package/dist/sdk/runtime/graph-inference.d.ts +1 -0
- package/dist/sdk/runtime/graph-inference.d.ts.map +1 -0
- package/dist/sdk/runtime/loader.d.ts +5 -7
- package/dist/sdk/runtime/loader.d.ts.map +1 -0
- package/dist/sdk/runtime/panel.d.ts +3 -2
- package/dist/sdk/runtime/panel.d.ts.map +1 -0
- package/dist/sdk/runtime/theme.d.ts +1 -0
- package/dist/sdk/runtime/theme.d.ts.map +1 -0
- package/dist/sdk/runtime/tmux.d.ts +26 -8
- package/dist/sdk/runtime/tmux.d.ts.map +1 -0
- package/dist/sdk/types.d.ts +23 -1
- package/dist/sdk/types.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.d.ts +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts +2 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/git.d.ts +1 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/git.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts +1 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/review.d.ts +2 -1
- package/dist/sdk/workflows/builtin/ralph/helpers/review.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts.map +1 -0
- package/dist/sdk/workflows/index.d.ts +14 -14
- package/dist/sdk/workflows/index.d.ts.map +1 -0
- package/dist/services/config/definitions.d.ts +85 -0
- package/dist/services/config/definitions.d.ts.map +1 -0
- package/dist/services/system/copy.d.ts +77 -0
- package/dist/services/system/copy.d.ts.map +1 -0
- package/dist/services/system/detect.d.ts +75 -0
- package/dist/services/system/detect.d.ts.map +1 -0
- package/package.json +13 -34
- package/src/cli.ts +11 -10
- package/src/commands/cli/chat/index.ts +11 -11
- package/src/commands/cli/chat.ts +1 -1
- package/src/commands/cli/config.ts +10 -9
- package/src/commands/cli/init/index.ts +11 -11
- package/src/commands/cli/init/onboarding.ts +4 -4
- package/src/commands/cli/init/scm.ts +5 -5
- package/src/commands/cli/init.ts +1 -1
- package/src/commands/cli/workflow-command.test.ts +19 -11
- package/src/commands/cli/workflow.test.ts +2 -2
- package/src/commands/cli/workflow.ts +6 -6
- package/src/lib/merge.ts +17 -31
- package/src/lib/path-root-guard.ts +2 -2
- package/src/lib/spawn.ts +13 -7
- package/src/scripts/bump-version.ts +1 -1
- package/src/scripts/constants.ts +2 -2
- package/src/sdk/components/header.tsx +21 -23
- package/src/sdk/components/hooks.ts +21 -0
- package/src/sdk/components/node-card.tsx +3 -2
- package/src/sdk/components/session-graph-panel.tsx +14 -18
- package/src/sdk/components/workflow-picker-panel.tsx +201 -216
- package/src/sdk/errors.test.ts +56 -0
- package/src/sdk/errors.ts +5 -0
- package/src/sdk/providers/claude.ts +279 -70
- package/src/sdk/providers/copilot.ts +17 -27
- package/src/sdk/providers/opencode.ts +17 -27
- package/src/sdk/runtime/discovery.ts +18 -18
- package/src/sdk/runtime/executor.test.ts +15 -48
- package/src/sdk/runtime/executor.ts +152 -121
- package/src/sdk/runtime/loader.ts +16 -21
- package/src/sdk/runtime/tmux.ts +95 -32
- package/src/sdk/types.ts +45 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +27 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.ts +25 -16
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/scout.ts +25 -24
- package/src/sdk/workflows/builtin/ralph/claude/index.ts +5 -0
- package/src/sdk/workflows/index.ts +3 -3
- package/src/services/config/atomic-config.ts +7 -8
- package/src/services/config/atomic-global-config.ts +9 -9
- package/src/services/config/config-path.ts +1 -1
- package/src/services/config/definitions.ts +3 -4
- package/src/services/config/index.ts +1 -1
- package/src/services/config/settings.ts +30 -36
- package/src/services/system/agents.ts +3 -3
- package/src/services/system/auto-sync.ts +9 -9
- package/src/services/system/copy.ts +9 -9
- package/src/services/system/file-lock.ts +2 -2
- package/src/services/system/install-ui.ts +2 -2
- package/src/services/system/skills.ts +1 -1
- package/src/theme/colors.ts +1 -1
- package/src/theme/logo.ts +1 -1
- package/tsconfig.json +3 -4
- package/dist/chunk-1gb5qxz9.js +0 -1
- package/dist/chunk-fdk7tact.js +0 -417
- package/dist/chunk-xkxndz5g.js +0 -1041
- package/dist/sdk/index.js +0 -52
- package/dist/sdk/workflows/builtin/ralph/claude/index.js +0 -96
- package/dist/sdk/workflows/builtin/ralph/copilot/index.js +0 -119
- package/dist/sdk/workflows/builtin/ralph/opencode/index.js +0 -148
- package/dist/sdk/workflows/index.js +0 -100
- package/src/commands/cli/chat/client.ts +0 -18
package/dist/chunk-xkxndz5g.js
DELETED
|
@@ -1,1041 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
// src/sdk/define-workflow.ts
|
|
3
|
-
function validateWorkflowInput(input, workflowName) {
|
|
4
|
-
if (!input.name || input.name.trim() === "") {
|
|
5
|
-
throw new Error(`Workflow "${workflowName}" has an input with an empty name.`);
|
|
6
|
-
}
|
|
7
|
-
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(input.name)) {
|
|
8
|
-
throw new Error(`Workflow "${workflowName}" input "${input.name}" has an invalid ` + `name \u2014 must start with a letter and contain only letters, ` + `digits, underscores, and dashes (so it can be used as a ` + `\`--${input.name}\` CLI flag).`);
|
|
9
|
-
}
|
|
10
|
-
if (input.type === "enum") {
|
|
11
|
-
if (!Array.isArray(input.values) || input.values.length === 0) {
|
|
12
|
-
throw new Error(`Workflow "${workflowName}" input "${input.name}" is an enum but ` + `declares no \`values\`.`);
|
|
13
|
-
}
|
|
14
|
-
if (input.default !== undefined && !input.values.includes(input.default)) {
|
|
15
|
-
throw new Error(`Workflow "${workflowName}" input "${input.name}" has a default ` + `"${input.default}" that is not one of its declared values: ` + `${input.values.join(", ")}.`);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
class WorkflowBuilder {
|
|
21
|
-
__brand = "WorkflowBuilder";
|
|
22
|
-
options;
|
|
23
|
-
runFn = null;
|
|
24
|
-
constructor(options) {
|
|
25
|
-
this.options = options;
|
|
26
|
-
}
|
|
27
|
-
run(fn) {
|
|
28
|
-
if (this.runFn) {
|
|
29
|
-
throw new Error("run() can only be called once per workflow.");
|
|
30
|
-
}
|
|
31
|
-
if (typeof fn !== "function") {
|
|
32
|
-
throw new Error(`run() requires a function, got ${typeof fn}.`);
|
|
33
|
-
}
|
|
34
|
-
this.runFn = fn;
|
|
35
|
-
return this;
|
|
36
|
-
}
|
|
37
|
-
compile() {
|
|
38
|
-
if (!this.runFn) {
|
|
39
|
-
throw new Error(`Workflow "${this.options.name}" has no run callback. ` + `Add a .run(async (ctx) => { ... }) call before .compile().`);
|
|
40
|
-
}
|
|
41
|
-
const runFn = this.runFn;
|
|
42
|
-
const declaredInputs = this.options.inputs ?? [];
|
|
43
|
-
const seen = new Set;
|
|
44
|
-
for (const input of declaredInputs) {
|
|
45
|
-
validateWorkflowInput(input, this.options.name);
|
|
46
|
-
if (seen.has(input.name)) {
|
|
47
|
-
throw new Error(`Workflow "${this.options.name}" has duplicate input name "${input.name}".`);
|
|
48
|
-
}
|
|
49
|
-
seen.add(input.name);
|
|
50
|
-
}
|
|
51
|
-
const inputs = Object.freeze(declaredInputs.map((i) => Object.freeze({ ...i })));
|
|
52
|
-
return {
|
|
53
|
-
__brand: "WorkflowDefinition",
|
|
54
|
-
name: this.options.name,
|
|
55
|
-
description: this.options.description ?? "",
|
|
56
|
-
inputs,
|
|
57
|
-
run: runFn
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
function defineWorkflow(options) {
|
|
62
|
-
if (!options.name || options.name.trim() === "") {
|
|
63
|
-
throw new Error("Workflow name is required.");
|
|
64
|
-
}
|
|
65
|
-
return new WorkflowBuilder(options);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// src/sdk/providers/copilot.ts
|
|
69
|
-
function validateCopilotWorkflow(source) {
|
|
70
|
-
const warnings = [];
|
|
71
|
-
if (/\bnew\s+CopilotClient\b/.test(source)) {
|
|
72
|
-
warnings.push({
|
|
73
|
-
rule: "copilot/manual-client",
|
|
74
|
-
message: "Manual CopilotClient creation detected. Use s.client instead \u2014 " + "the runtime auto-creates and cleans up the client."
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
if (/\bclient\.createSession\b/.test(source)) {
|
|
78
|
-
warnings.push({
|
|
79
|
-
rule: "copilot/manual-session",
|
|
80
|
-
message: "Manual createSession() call detected. Use s.session instead \u2014 " + "the runtime auto-creates the session. Pass session config as the third arg to ctx.stage()."
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
return warnings;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// src/sdk/providers/opencode.ts
|
|
87
|
-
function validateOpenCodeWorkflow(source) {
|
|
88
|
-
const warnings = [];
|
|
89
|
-
if (/\bcreateOpencodeClient\b/.test(source)) {
|
|
90
|
-
warnings.push({
|
|
91
|
-
rule: "opencode/manual-client",
|
|
92
|
-
message: "Manual createOpencodeClient() call detected. Use s.client instead \u2014 " + "the runtime auto-creates the client. Pass client config as the second arg to ctx.stage()."
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
if (/\bclient\.session\.create\b/.test(source)) {
|
|
96
|
-
warnings.push({
|
|
97
|
-
rule: "opencode/manual-session",
|
|
98
|
-
message: "Manual client.session.create() call detected. Use s.session instead \u2014 " + "the runtime auto-creates the session. Pass session config as the third arg to ctx.stage()."
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
return warnings;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// src/sdk/runtime/tmux.ts
|
|
105
|
-
import { join } from "path";
|
|
106
|
-
var SOCKET_NAME = "atomic";
|
|
107
|
-
var CONFIG_PATH = join(import.meta.dir, "tmux.conf");
|
|
108
|
-
var resolvedMuxBinary;
|
|
109
|
-
function getMuxBinary() {
|
|
110
|
-
if (resolvedMuxBinary !== undefined)
|
|
111
|
-
return resolvedMuxBinary;
|
|
112
|
-
if (process.platform === "win32") {
|
|
113
|
-
for (const candidate of ["psmux", "pmux", "tmux"]) {
|
|
114
|
-
if (Bun.which(candidate)) {
|
|
115
|
-
resolvedMuxBinary = candidate;
|
|
116
|
-
return resolvedMuxBinary;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
resolvedMuxBinary = null;
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
resolvedMuxBinary = Bun.which("tmux") ? "tmux" : null;
|
|
123
|
-
return resolvedMuxBinary;
|
|
124
|
-
}
|
|
125
|
-
function resetMuxBinaryCache() {
|
|
126
|
-
resolvedMuxBinary = undefined;
|
|
127
|
-
}
|
|
128
|
-
function isTmuxInstalled() {
|
|
129
|
-
return getMuxBinary() !== null;
|
|
130
|
-
}
|
|
131
|
-
function isInsideTmux() {
|
|
132
|
-
return process.env.TMUX !== undefined || process.env.PSMUX !== undefined;
|
|
133
|
-
}
|
|
134
|
-
function tmuxRun(args) {
|
|
135
|
-
const binary = getMuxBinary();
|
|
136
|
-
if (!binary) {
|
|
137
|
-
return { ok: false, stderr: "No terminal multiplexer (tmux/psmux) found on PATH" };
|
|
138
|
-
}
|
|
139
|
-
const fullArgs = ["-f", CONFIG_PATH, "-L", SOCKET_NAME, ...args];
|
|
140
|
-
const result = Bun.spawnSync({
|
|
141
|
-
cmd: [binary, ...fullArgs],
|
|
142
|
-
stdout: "pipe",
|
|
143
|
-
stderr: "pipe"
|
|
144
|
-
});
|
|
145
|
-
if (!result.success) {
|
|
146
|
-
const stderr = new TextDecoder().decode(result.stderr).trim();
|
|
147
|
-
return { ok: false, stderr };
|
|
148
|
-
}
|
|
149
|
-
return { ok: true, stdout: new TextDecoder().decode(result.stdout).trim() };
|
|
150
|
-
}
|
|
151
|
-
function tmux(args) {
|
|
152
|
-
const result = tmuxRun(args);
|
|
153
|
-
if (!result.ok) {
|
|
154
|
-
throw new Error(`tmux ${args[0]} failed: ${result.stderr}`);
|
|
155
|
-
}
|
|
156
|
-
return result.stdout;
|
|
157
|
-
}
|
|
158
|
-
function tmuxExec(args) {
|
|
159
|
-
const result = tmuxRun(args);
|
|
160
|
-
if (!result.ok) {
|
|
161
|
-
throw new Error(`tmux ${args[0]} failed: ${result.stderr}`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
function buildEnvArgs(envVars) {
|
|
165
|
-
if (!envVars)
|
|
166
|
-
return [];
|
|
167
|
-
const args = [];
|
|
168
|
-
for (const [key, value] of Object.entries(envVars)) {
|
|
169
|
-
args.push("-e", `${key}=${value}`);
|
|
170
|
-
}
|
|
171
|
-
return args;
|
|
172
|
-
}
|
|
173
|
-
function createSession(sessionName, initialCommand, windowName, cwd, envVars) {
|
|
174
|
-
const args = [
|
|
175
|
-
"new-session",
|
|
176
|
-
"-d",
|
|
177
|
-
"-s",
|
|
178
|
-
sessionName,
|
|
179
|
-
"-P",
|
|
180
|
-
"-F",
|
|
181
|
-
"#{pane_id}",
|
|
182
|
-
...buildEnvArgs(envVars)
|
|
183
|
-
];
|
|
184
|
-
if (windowName) {
|
|
185
|
-
args.push("-n", windowName);
|
|
186
|
-
}
|
|
187
|
-
if (cwd) {
|
|
188
|
-
args.push("-c", cwd);
|
|
189
|
-
}
|
|
190
|
-
args.push(initialCommand);
|
|
191
|
-
const paneId = tmux(args);
|
|
192
|
-
return paneId || tmux(["list-panes", "-t", sessionName, "-F", "#{pane_id}"]).split(`
|
|
193
|
-
`)[0];
|
|
194
|
-
}
|
|
195
|
-
function createWindow(sessionName, windowName, command, cwd, envVars) {
|
|
196
|
-
const args = [
|
|
197
|
-
"new-window",
|
|
198
|
-
"-d",
|
|
199
|
-
"-t",
|
|
200
|
-
sessionName,
|
|
201
|
-
"-n",
|
|
202
|
-
windowName,
|
|
203
|
-
"-P",
|
|
204
|
-
"-F",
|
|
205
|
-
"#{pane_id}",
|
|
206
|
-
...buildEnvArgs(envVars)
|
|
207
|
-
];
|
|
208
|
-
if (cwd) {
|
|
209
|
-
args.push("-c", cwd);
|
|
210
|
-
}
|
|
211
|
-
args.push(command);
|
|
212
|
-
return tmux(args);
|
|
213
|
-
}
|
|
214
|
-
function createPane(sessionName, command) {
|
|
215
|
-
return tmux([
|
|
216
|
-
"split-window",
|
|
217
|
-
"-t",
|
|
218
|
-
sessionName,
|
|
219
|
-
"-P",
|
|
220
|
-
"-F",
|
|
221
|
-
"#{pane_id}",
|
|
222
|
-
command
|
|
223
|
-
]);
|
|
224
|
-
}
|
|
225
|
-
function sendLiteralText(paneId, text) {
|
|
226
|
-
const normalized = text.replace(/[\r\n]+/g, " ");
|
|
227
|
-
tmuxExec(["send-keys", "-t", paneId, "-l", "--", normalized]);
|
|
228
|
-
}
|
|
229
|
-
function sendSpecialKey(paneId, key) {
|
|
230
|
-
tmuxExec(["send-keys", "-t", paneId, key]);
|
|
231
|
-
}
|
|
232
|
-
function sendKeysAndSubmit(paneId, text, presses = 1, delayMs = 100) {
|
|
233
|
-
sendLiteralText(paneId, text);
|
|
234
|
-
for (let i = 0;i < presses; i++) {
|
|
235
|
-
if (i > 0 && delayMs > 0) {
|
|
236
|
-
Bun.sleepSync(delayMs);
|
|
237
|
-
}
|
|
238
|
-
sendSpecialKey(paneId, "C-m");
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
function capturePane(paneId, start) {
|
|
242
|
-
const args = ["capture-pane", "-t", paneId, "-p"];
|
|
243
|
-
if (start !== undefined) {
|
|
244
|
-
args.push("-S", String(start));
|
|
245
|
-
}
|
|
246
|
-
return tmux(args);
|
|
247
|
-
}
|
|
248
|
-
function capturePaneVisible(paneId) {
|
|
249
|
-
const result = tmuxRun(["capture-pane", "-t", paneId, "-p"]);
|
|
250
|
-
if (!result.ok)
|
|
251
|
-
return "";
|
|
252
|
-
return result.stdout;
|
|
253
|
-
}
|
|
254
|
-
function capturePaneScrollback(paneId, lines = 200) {
|
|
255
|
-
const result = tmuxRun(["capture-pane", "-t", paneId, "-p", "-S", `-${lines}`]);
|
|
256
|
-
if (!result.ok)
|
|
257
|
-
return "";
|
|
258
|
-
return result.stdout;
|
|
259
|
-
}
|
|
260
|
-
function killSession(sessionName) {
|
|
261
|
-
try {
|
|
262
|
-
tmuxExec(["kill-session", "-t", sessionName]);
|
|
263
|
-
} catch {}
|
|
264
|
-
}
|
|
265
|
-
function killWindow(sessionName, windowName) {
|
|
266
|
-
try {
|
|
267
|
-
tmuxExec(["kill-window", "-t", `${sessionName}:${windowName}`]);
|
|
268
|
-
} catch {}
|
|
269
|
-
}
|
|
270
|
-
function sessionExists(sessionName) {
|
|
271
|
-
const result = tmuxRun(["has-session", "-t", sessionName]);
|
|
272
|
-
return result.ok;
|
|
273
|
-
}
|
|
274
|
-
function attachSession(sessionName) {
|
|
275
|
-
const binary = getMuxBinary();
|
|
276
|
-
if (!binary) {
|
|
277
|
-
throw new Error("No terminal multiplexer (tmux/psmux) found on PATH");
|
|
278
|
-
}
|
|
279
|
-
const proc = Bun.spawnSync({
|
|
280
|
-
cmd: [binary, "-f", CONFIG_PATH, "-L", SOCKET_NAME, "attach-session", "-t", sessionName],
|
|
281
|
-
stdin: "inherit",
|
|
282
|
-
stdout: "inherit",
|
|
283
|
-
stderr: "pipe"
|
|
284
|
-
});
|
|
285
|
-
if (!proc.success) {
|
|
286
|
-
const stderr = new TextDecoder().decode(proc.stderr).trim();
|
|
287
|
-
throw new Error(`Failed to attach to session: ${sessionName}${stderr ? ` (${stderr})` : ""}`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
function spawnMuxAttach(sessionName) {
|
|
291
|
-
const binary = getMuxBinary();
|
|
292
|
-
if (!binary) {
|
|
293
|
-
throw new Error("No terminal multiplexer (tmux/psmux) found on PATH");
|
|
294
|
-
}
|
|
295
|
-
return Bun.spawn([binary, "-f", CONFIG_PATH, "-L", SOCKET_NAME, "attach-session", "-t", sessionName], { stdio: ["inherit", "inherit", "inherit"] });
|
|
296
|
-
}
|
|
297
|
-
function switchClient(sessionName) {
|
|
298
|
-
tmuxExec(["switch-client", "-t", sessionName]);
|
|
299
|
-
}
|
|
300
|
-
function getCurrentSession() {
|
|
301
|
-
if (!isInsideTmux())
|
|
302
|
-
return null;
|
|
303
|
-
const result = tmuxRun(["display-message", "-p", "#{session_name}"]);
|
|
304
|
-
if (!result.ok)
|
|
305
|
-
return null;
|
|
306
|
-
return result.stdout || null;
|
|
307
|
-
}
|
|
308
|
-
function attachOrSwitch(sessionName) {
|
|
309
|
-
if (isInsideTmux()) {
|
|
310
|
-
switchClient(sessionName);
|
|
311
|
-
} else {
|
|
312
|
-
attachSession(sessionName);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
function selectWindow(target) {
|
|
316
|
-
tmuxExec(["select-window", "-t", target]);
|
|
317
|
-
}
|
|
318
|
-
function normalizeTmuxCapture(text) {
|
|
319
|
-
return text.replace(/\r/g, "").replace(/\s+/g, " ").trim();
|
|
320
|
-
}
|
|
321
|
-
function normalizeTmuxLines(text) {
|
|
322
|
-
return text.split(`
|
|
323
|
-
`).map((l) => l.trimEnd()).join(`
|
|
324
|
-
`).trim();
|
|
325
|
-
}
|
|
326
|
-
function toPaneLines(captured) {
|
|
327
|
-
return captured.split(`
|
|
328
|
-
`).map((l) => l.replace(/\r/g, "").trimEnd()).filter((l) => l.trim() !== "");
|
|
329
|
-
}
|
|
330
|
-
function paneIsBootstrapping(lines) {
|
|
331
|
-
return lines.some((line) => /\b(loading|initializing|starting up)\b/i.test(line) || /\bmodel:\s*loading\b/i.test(line) || /\bconnecting\s+to\b/i.test(line));
|
|
332
|
-
}
|
|
333
|
-
function paneLooksReady(captured) {
|
|
334
|
-
const content = captured.trimEnd();
|
|
335
|
-
if (content === "")
|
|
336
|
-
return false;
|
|
337
|
-
const lines = toPaneLines(content);
|
|
338
|
-
if (paneIsBootstrapping(lines))
|
|
339
|
-
return false;
|
|
340
|
-
if (lines.some((line) => /^\s*[\u203A>\u276F]\s*/u.test(line)))
|
|
341
|
-
return true;
|
|
342
|
-
if (lines.some((line) => /\bhow can i help(?: you)?\b/i.test(line)))
|
|
343
|
-
return true;
|
|
344
|
-
return false;
|
|
345
|
-
}
|
|
346
|
-
function paneHasActiveTask(captured) {
|
|
347
|
-
const tail = toPaneLines(captured).map((line) => line.trim()).slice(-40);
|
|
348
|
-
if (tail.some((l) => /\b\d+\s+background terminal running\b/i.test(l)))
|
|
349
|
-
return true;
|
|
350
|
-
if (tail.some((l) => /esc to interrupt/i.test(l)))
|
|
351
|
-
return true;
|
|
352
|
-
if (tail.some((l) => /\bbackground terminal running\b/i.test(l)))
|
|
353
|
-
return true;
|
|
354
|
-
return tail.some((l) => /^[\u00B7\u273B]\s+[A-Za-z][A-Za-z0-9''-]*(?:\s+[A-Za-z][A-Za-z0-9''-]*){0,3}(?:\u2026|\.{3})$/u.test(l));
|
|
355
|
-
}
|
|
356
|
-
function paneIsIdle(paneId) {
|
|
357
|
-
const visible = capturePaneVisible(paneId);
|
|
358
|
-
return paneLooksReady(visible) && !paneHasActiveTask(visible);
|
|
359
|
-
}
|
|
360
|
-
async function waitForPaneReady(paneId, timeoutMs = 30000) {
|
|
361
|
-
const startedAt = Date.now();
|
|
362
|
-
let delayMs = 150;
|
|
363
|
-
const maxDelayMs = 8000;
|
|
364
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
365
|
-
if (paneIsIdle(paneId))
|
|
366
|
-
return Date.now() - startedAt;
|
|
367
|
-
const remaining = timeoutMs - (Date.now() - startedAt);
|
|
368
|
-
if (remaining <= 0)
|
|
369
|
-
break;
|
|
370
|
-
await Bun.sleep(Math.min(delayMs, remaining));
|
|
371
|
-
delayMs = Math.min(maxDelayMs, delayMs * 2);
|
|
372
|
-
}
|
|
373
|
-
return Date.now() - startedAt;
|
|
374
|
-
}
|
|
375
|
-
async function attemptSubmitRounds(paneId, normalizedPrompt, rounds, pressesPerRound = 1) {
|
|
376
|
-
const presses = Math.max(1, Math.floor(pressesPerRound));
|
|
377
|
-
for (let round = 0;round < rounds; round++) {
|
|
378
|
-
await Bun.sleep(100);
|
|
379
|
-
for (let press = 0;press < presses; press++) {
|
|
380
|
-
sendSpecialKey(paneId, "C-m");
|
|
381
|
-
if (press < presses - 1)
|
|
382
|
-
await Bun.sleep(200);
|
|
383
|
-
}
|
|
384
|
-
await Bun.sleep(140);
|
|
385
|
-
const visible = capturePaneVisible(paneId);
|
|
386
|
-
if (!normalizeTmuxCapture(visible).includes(normalizedPrompt))
|
|
387
|
-
return true;
|
|
388
|
-
if (paneHasActiveTask(visible))
|
|
389
|
-
return true;
|
|
390
|
-
await Bun.sleep(140);
|
|
391
|
-
}
|
|
392
|
-
return false;
|
|
393
|
-
}
|
|
394
|
-
async function waitForOutput(paneId, pattern, options = {}) {
|
|
395
|
-
const { timeoutMs = 30000, pollIntervalMs = 500 } = options;
|
|
396
|
-
const deadline = Date.now() + timeoutMs;
|
|
397
|
-
while (Date.now() < deadline) {
|
|
398
|
-
const content = capturePane(paneId);
|
|
399
|
-
if (pattern.test(content)) {
|
|
400
|
-
return content;
|
|
401
|
-
}
|
|
402
|
-
await Bun.sleep(pollIntervalMs);
|
|
403
|
-
}
|
|
404
|
-
throw new Error(`Timed out waiting for pattern ${pattern} in pane ${paneId}`);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// src/sdk/providers/claude.ts
|
|
408
|
-
var initializedPanes = new Set;
|
|
409
|
-
function clearClaudeSession(paneId) {
|
|
410
|
-
initializedPanes.delete(paneId);
|
|
411
|
-
}
|
|
412
|
-
var DEFAULT_CHAT_FLAGS = [
|
|
413
|
-
"--allow-dangerously-skip-permissions",
|
|
414
|
-
"--dangerously-skip-permissions"
|
|
415
|
-
];
|
|
416
|
-
async function createClaudeSession(options) {
|
|
417
|
-
const {
|
|
418
|
-
paneId,
|
|
419
|
-
chatFlags = DEFAULT_CHAT_FLAGS,
|
|
420
|
-
readyTimeoutMs = 30000
|
|
421
|
-
} = options;
|
|
422
|
-
const cmd = ["claude", ...chatFlags].join(" ");
|
|
423
|
-
sendKeysAndSubmit(paneId, cmd);
|
|
424
|
-
await Bun.sleep(1000);
|
|
425
|
-
await waitForPaneReady(paneId, readyTimeoutMs);
|
|
426
|
-
const visible = capturePaneVisible(paneId);
|
|
427
|
-
if (!paneLooksReady(visible) && !paneHasActiveTask(visible)) {
|
|
428
|
-
throw new Error("createClaudeSession() timed out waiting for the Claude TUI to start. " + "Verify the `claude` command is installed and the flags are valid.");
|
|
429
|
-
}
|
|
430
|
-
initializedPanes.add(paneId);
|
|
431
|
-
}
|
|
432
|
-
async function claudeQuery(options) {
|
|
433
|
-
const {
|
|
434
|
-
paneId,
|
|
435
|
-
prompt,
|
|
436
|
-
timeoutMs = 300000,
|
|
437
|
-
pollIntervalMs = 2000,
|
|
438
|
-
submitPresses = 1,
|
|
439
|
-
maxSubmitRounds = 6,
|
|
440
|
-
readyTimeoutMs = 30000
|
|
441
|
-
} = options;
|
|
442
|
-
if (!initializedPanes.has(paneId)) {
|
|
443
|
-
throw new Error("claudeQuery() called without a prior createClaudeSession() for this pane. " + "Call createClaudeSession({ paneId }) first to start the Claude CLI.");
|
|
444
|
-
}
|
|
445
|
-
const normalizedPrompt = normalizeTmuxCapture(prompt).slice(0, 100);
|
|
446
|
-
const waitElapsed = await waitForPaneReady(paneId, readyTimeoutMs);
|
|
447
|
-
const responseTimeoutMs = Math.max(0, timeoutMs - waitElapsed);
|
|
448
|
-
if (waitElapsed > timeoutMs * 0.5) {
|
|
449
|
-
console.warn(`claudeQuery: readiness wait consumed ${Math.round(waitElapsed / 1000)}s ` + `of ${Math.round(timeoutMs / 1000)}s total timeout budget`);
|
|
450
|
-
}
|
|
451
|
-
const beforeContent = normalizeTmuxLines(capturePaneScrollback(paneId));
|
|
452
|
-
sendLiteralText(paneId, prompt);
|
|
453
|
-
await Bun.sleep(150);
|
|
454
|
-
let delivered = await attemptSubmitRounds(paneId, normalizedPrompt, maxSubmitRounds, submitPresses);
|
|
455
|
-
if (!delivered) {
|
|
456
|
-
const visibleCapture = capturePaneVisible(paneId);
|
|
457
|
-
const visibleNorm = normalizeTmuxCapture(visibleCapture);
|
|
458
|
-
if (visibleNorm.includes(normalizedPrompt) && !paneHasActiveTask(visibleCapture) && paneLooksReady(visibleCapture)) {
|
|
459
|
-
sendSpecialKey(paneId, "C-u");
|
|
460
|
-
await Bun.sleep(80);
|
|
461
|
-
sendLiteralText(paneId, prompt);
|
|
462
|
-
await Bun.sleep(120);
|
|
463
|
-
delivered = await attemptSubmitRounds(paneId, normalizedPrompt, 4, submitPresses);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
if (!delivered) {
|
|
467
|
-
sendSpecialKey(paneId, "C-m");
|
|
468
|
-
await Bun.sleep(120);
|
|
469
|
-
sendSpecialKey(paneId, "C-m");
|
|
470
|
-
await Bun.sleep(300);
|
|
471
|
-
const verifyCapture = capturePaneVisible(paneId);
|
|
472
|
-
if (paneHasActiveTask(verifyCapture)) {
|
|
473
|
-
delivered = true;
|
|
474
|
-
} else {
|
|
475
|
-
delivered = !normalizeTmuxCapture(verifyCapture).includes(normalizedPrompt);
|
|
476
|
-
}
|
|
477
|
-
if (!delivered) {
|
|
478
|
-
sendSpecialKey(paneId, "C-m");
|
|
479
|
-
await Bun.sleep(150);
|
|
480
|
-
sendSpecialKey(paneId, "C-m");
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
const deadline = Date.now() + responseTimeoutMs;
|
|
484
|
-
let lastContent = "";
|
|
485
|
-
let stableCount = 0;
|
|
486
|
-
await Bun.sleep(3000);
|
|
487
|
-
while (Date.now() < deadline) {
|
|
488
|
-
const currentContent = normalizeTmuxLines(capturePaneScrollback(paneId));
|
|
489
|
-
if (currentContent === beforeContent) {
|
|
490
|
-
await Bun.sleep(pollIntervalMs);
|
|
491
|
-
continue;
|
|
492
|
-
}
|
|
493
|
-
const visible = capturePaneVisible(paneId);
|
|
494
|
-
if (paneLooksReady(visible) && !paneHasActiveTask(visible)) {
|
|
495
|
-
return { output: currentContent, delivered };
|
|
496
|
-
}
|
|
497
|
-
if (currentContent === lastContent) {
|
|
498
|
-
stableCount++;
|
|
499
|
-
if (stableCount >= 3) {
|
|
500
|
-
return { output: currentContent, delivered };
|
|
501
|
-
}
|
|
502
|
-
} else {
|
|
503
|
-
stableCount = 0;
|
|
504
|
-
}
|
|
505
|
-
lastContent = currentContent;
|
|
506
|
-
await Bun.sleep(pollIntervalMs);
|
|
507
|
-
}
|
|
508
|
-
return { output: lastContent || capturePaneScrollback(paneId), delivered };
|
|
509
|
-
}
|
|
510
|
-
function validateClaudeWorkflow(source) {
|
|
511
|
-
const warnings = [];
|
|
512
|
-
if (/\bcreateClaudeSession\b/.test(source)) {
|
|
513
|
-
warnings.push({
|
|
514
|
-
rule: "claude/manual-session",
|
|
515
|
-
message: "Manual createClaudeSession() call detected. The runtime auto-starts the Claude CLI \u2014 " + "use s.session.query() instead of claudeQuery(). Pass chatFlags via the second arg to ctx.stage()."
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
if (/\bclaudeQuery\b/.test(source)) {
|
|
519
|
-
warnings.push({
|
|
520
|
-
rule: "claude/manual-query",
|
|
521
|
-
message: "Direct claudeQuery() call detected. Use s.session.query(prompt) instead \u2014 " + "it wraps claudeQuery with the correct paneId."
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
return warnings;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// src/sdk/runtime/loader.ts
|
|
528
|
-
var WorkflowLoader;
|
|
529
|
-
((WorkflowLoader) => {
|
|
530
|
-
async function resolve(plan) {
|
|
531
|
-
try {
|
|
532
|
-
const file = Bun.file(plan.path);
|
|
533
|
-
if (!await file.exists()) {
|
|
534
|
-
return {
|
|
535
|
-
ok: false,
|
|
536
|
-
stage: "resolve",
|
|
537
|
-
error: new Error(`Workflow file not found: ${plan.path}`),
|
|
538
|
-
message: `Workflow file not found: ${plan.path}`
|
|
539
|
-
};
|
|
540
|
-
}
|
|
541
|
-
return { ok: true, value: plan };
|
|
542
|
-
} catch (error) {
|
|
543
|
-
return {
|
|
544
|
-
ok: false,
|
|
545
|
-
stage: "resolve",
|
|
546
|
-
error,
|
|
547
|
-
message: error instanceof Error ? error.message : String(error)
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
WorkflowLoader.resolve = resolve;
|
|
552
|
-
function validateSource(source, agent) {
|
|
553
|
-
switch (agent) {
|
|
554
|
-
case "copilot":
|
|
555
|
-
return validateCopilotWorkflow(source);
|
|
556
|
-
case "opencode":
|
|
557
|
-
return validateOpenCodeWorkflow(source);
|
|
558
|
-
case "claude":
|
|
559
|
-
return validateClaudeWorkflow(source);
|
|
560
|
-
default:
|
|
561
|
-
return [];
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
async function validate(resolved) {
|
|
565
|
-
try {
|
|
566
|
-
const source = await Bun.file(resolved.path).text();
|
|
567
|
-
const warnings = validateSource(source, resolved.agent);
|
|
568
|
-
return {
|
|
569
|
-
ok: true,
|
|
570
|
-
value: { ...resolved, warnings }
|
|
571
|
-
};
|
|
572
|
-
} catch (error) {
|
|
573
|
-
return {
|
|
574
|
-
ok: false,
|
|
575
|
-
stage: "validate",
|
|
576
|
-
error,
|
|
577
|
-
message: error instanceof Error ? error.message : String(error)
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
WorkflowLoader.validate = validate;
|
|
582
|
-
async function load(validated) {
|
|
583
|
-
try {
|
|
584
|
-
const mod = await import(validated.path);
|
|
585
|
-
const definition = mod.default ?? mod;
|
|
586
|
-
if (!definition || definition.__brand !== "WorkflowDefinition") {
|
|
587
|
-
if (definition && definition.__brand === "WorkflowBuilder") {
|
|
588
|
-
return {
|
|
589
|
-
ok: false,
|
|
590
|
-
stage: "load",
|
|
591
|
-
error: new Error("Workflow not compiled"),
|
|
592
|
-
message: `Workflow at ${validated.path} was defined but not compiled.
|
|
593
|
-
Add .compile() at the end of your defineWorkflow() chain:
|
|
594
|
-
|
|
595
|
-
export default defineWorkflow({ ... })
|
|
596
|
-
.run(async (ctx) => { ... })
|
|
597
|
-
.compile();`
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
return {
|
|
601
|
-
ok: false,
|
|
602
|
-
stage: "load",
|
|
603
|
-
error: new Error("Invalid workflow export"),
|
|
604
|
-
message: `${validated.path} does not export a valid WorkflowDefinition.
|
|
605
|
-
Make sure it exports defineWorkflow(...).run(...).compile() as the default export.`
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
return {
|
|
609
|
-
ok: true,
|
|
610
|
-
value: { ...validated, definition }
|
|
611
|
-
};
|
|
612
|
-
} catch (error) {
|
|
613
|
-
return {
|
|
614
|
-
ok: false,
|
|
615
|
-
stage: "load",
|
|
616
|
-
error,
|
|
617
|
-
message: error instanceof Error ? error.message : String(error)
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
WorkflowLoader.load = load;
|
|
622
|
-
async function loadWorkflow(plan, report) {
|
|
623
|
-
report?.start?.("resolve");
|
|
624
|
-
const resolved = await resolve(plan);
|
|
625
|
-
if (!resolved.ok) {
|
|
626
|
-
report?.error?.("resolve", resolved.error, resolved.message);
|
|
627
|
-
return resolved;
|
|
628
|
-
}
|
|
629
|
-
report?.start?.("validate");
|
|
630
|
-
const validated = await validate(resolved.value);
|
|
631
|
-
if (!validated.ok) {
|
|
632
|
-
report?.error?.("validate", validated.error, validated.message);
|
|
633
|
-
return validated;
|
|
634
|
-
}
|
|
635
|
-
if (validated.value.warnings.length > 0) {
|
|
636
|
-
report?.warn?.(validated.value.warnings);
|
|
637
|
-
}
|
|
638
|
-
report?.start?.("load");
|
|
639
|
-
const loaded = await load(validated.value);
|
|
640
|
-
if (!loaded.ok) {
|
|
641
|
-
report?.error?.("load", loaded.error, loaded.message);
|
|
642
|
-
return loaded;
|
|
643
|
-
}
|
|
644
|
-
return loaded;
|
|
645
|
-
}
|
|
646
|
-
WorkflowLoader.loadWorkflow = loadWorkflow;
|
|
647
|
-
})(WorkflowLoader ||= {});
|
|
648
|
-
|
|
649
|
-
// src/sdk/runtime/discovery.ts
|
|
650
|
-
import { join as join2 } from "path";
|
|
651
|
-
import { readdir, writeFile } from "fs/promises";
|
|
652
|
-
import { existsSync, readdirSync } from "fs";
|
|
653
|
-
import { homedir } from "os";
|
|
654
|
-
import ignore from "ignore";
|
|
655
|
-
function getLocalWorkflowsDir(projectRoot) {
|
|
656
|
-
return join2(projectRoot, ".atomic", "workflows");
|
|
657
|
-
}
|
|
658
|
-
function getGlobalWorkflowsDir() {
|
|
659
|
-
return join2(homedir(), ".atomic", "workflows");
|
|
660
|
-
}
|
|
661
|
-
var AGENTS = ["copilot", "opencode", "claude"];
|
|
662
|
-
var AGENT_SET = new Set(AGENTS);
|
|
663
|
-
var WORKFLOWS_GITIGNORE = [
|
|
664
|
-
"node_modules/",
|
|
665
|
-
"dist/",
|
|
666
|
-
"build/",
|
|
667
|
-
"coverage/",
|
|
668
|
-
".cache/",
|
|
669
|
-
"*.log",
|
|
670
|
-
"*.tsbuildinfo",
|
|
671
|
-
""
|
|
672
|
-
].join(`
|
|
673
|
-
`);
|
|
674
|
-
async function loadWorkflowsGitignore(workflowsDir) {
|
|
675
|
-
const gitignorePath = join2(workflowsDir, ".gitignore");
|
|
676
|
-
let content;
|
|
677
|
-
try {
|
|
678
|
-
content = await Bun.file(gitignorePath).text();
|
|
679
|
-
} catch {
|
|
680
|
-
await writeFile(gitignorePath, WORKFLOWS_GITIGNORE);
|
|
681
|
-
content = WORKFLOWS_GITIGNORE;
|
|
682
|
-
}
|
|
683
|
-
return ignore().add(content);
|
|
684
|
-
}
|
|
685
|
-
async function discoverFromBaseDir(baseDir, source, agentFilter) {
|
|
686
|
-
const workflows = [];
|
|
687
|
-
const agents = agentFilter ? [agentFilter] : AGENTS;
|
|
688
|
-
const agentNames = new Set(agents);
|
|
689
|
-
let workflowEntries;
|
|
690
|
-
try {
|
|
691
|
-
workflowEntries = await readdir(baseDir, { withFileTypes: true });
|
|
692
|
-
} catch {
|
|
693
|
-
return workflows;
|
|
694
|
-
}
|
|
695
|
-
const ig = await loadWorkflowsGitignore(baseDir);
|
|
696
|
-
for (const wfEntry of workflowEntries) {
|
|
697
|
-
if (!wfEntry.isDirectory())
|
|
698
|
-
continue;
|
|
699
|
-
if (wfEntry.name.startsWith("."))
|
|
700
|
-
continue;
|
|
701
|
-
if (AGENT_SET.has(wfEntry.name))
|
|
702
|
-
continue;
|
|
703
|
-
if (ig.ignores(wfEntry.name + "/"))
|
|
704
|
-
continue;
|
|
705
|
-
const workflowDir = join2(baseDir, wfEntry.name);
|
|
706
|
-
let agentEntries;
|
|
707
|
-
try {
|
|
708
|
-
agentEntries = await readdir(workflowDir, { withFileTypes: true });
|
|
709
|
-
} catch {
|
|
710
|
-
continue;
|
|
711
|
-
}
|
|
712
|
-
for (const agentEntry of agentEntries) {
|
|
713
|
-
if (!agentEntry.isDirectory())
|
|
714
|
-
continue;
|
|
715
|
-
if (!agentNames.has(agentEntry.name))
|
|
716
|
-
continue;
|
|
717
|
-
const indexPath = join2(workflowDir, agentEntry.name, "index.ts");
|
|
718
|
-
const file = Bun.file(indexPath);
|
|
719
|
-
if (await file.exists()) {
|
|
720
|
-
workflows.push({
|
|
721
|
-
name: wfEntry.name,
|
|
722
|
-
agent: agentEntry.name,
|
|
723
|
-
path: indexPath,
|
|
724
|
-
source
|
|
725
|
-
});
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
return workflows;
|
|
730
|
-
}
|
|
731
|
-
var BUILTIN_WORKFLOWS_DIR = join2(Bun.fileURLToPath(new URL("../workflows/builtin", import.meta.url)));
|
|
732
|
-
function discoverBuiltinWorkflows(agentFilter) {
|
|
733
|
-
const results = [];
|
|
734
|
-
const agents = agentFilter ? [agentFilter] : AGENTS;
|
|
735
|
-
let workflowNames;
|
|
736
|
-
try {
|
|
737
|
-
workflowNames = readdirSync(BUILTIN_WORKFLOWS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
738
|
-
} catch {
|
|
739
|
-
return results;
|
|
740
|
-
}
|
|
741
|
-
for (const name of workflowNames) {
|
|
742
|
-
for (const agent of agents) {
|
|
743
|
-
const indexPath = join2(BUILTIN_WORKFLOWS_DIR, name, agent, "index.ts");
|
|
744
|
-
if (existsSync(indexPath)) {
|
|
745
|
-
results.push({ name, agent, path: indexPath, source: "builtin" });
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
return results;
|
|
750
|
-
}
|
|
751
|
-
async function discoverWorkflows(projectRoot = process.cwd(), agentFilter, options = {}) {
|
|
752
|
-
const { merge = true } = options;
|
|
753
|
-
const localDir = getLocalWorkflowsDir(projectRoot);
|
|
754
|
-
const globalDir = getGlobalWorkflowsDir();
|
|
755
|
-
const allBuiltins = discoverBuiltinWorkflows();
|
|
756
|
-
const reservedNames = new Set(allBuiltins.map((w) => w.name));
|
|
757
|
-
const builtinResults = agentFilter ? allBuiltins.filter((w) => w.agent === agentFilter) : allBuiltins;
|
|
758
|
-
const [globalResults, localResults] = await Promise.all([
|
|
759
|
-
discoverFromBaseDir(globalDir, "global", agentFilter),
|
|
760
|
-
discoverFromBaseDir(localDir, "local", agentFilter)
|
|
761
|
-
]);
|
|
762
|
-
const filteredGlobal = globalResults.filter((w) => !reservedNames.has(w.name));
|
|
763
|
-
const filteredLocal = localResults.filter((w) => !reservedNames.has(w.name));
|
|
764
|
-
if (!merge) {
|
|
765
|
-
return [...filteredGlobal, ...filteredLocal, ...builtinResults];
|
|
766
|
-
}
|
|
767
|
-
const byKey = new Map;
|
|
768
|
-
for (const wf of filteredGlobal) {
|
|
769
|
-
byKey.set(`${wf.agent}/${wf.name}`, wf);
|
|
770
|
-
}
|
|
771
|
-
for (const wf of filteredLocal) {
|
|
772
|
-
byKey.set(`${wf.agent}/${wf.name}`, wf);
|
|
773
|
-
}
|
|
774
|
-
for (const wf of builtinResults) {
|
|
775
|
-
byKey.set(`${wf.agent}/${wf.name}`, wf);
|
|
776
|
-
}
|
|
777
|
-
return Array.from(byKey.values());
|
|
778
|
-
}
|
|
779
|
-
async function findWorkflow(name, agent, projectRoot = process.cwd()) {
|
|
780
|
-
const all = await discoverWorkflows(projectRoot, agent);
|
|
781
|
-
return all.find((w) => w.name === name) ?? null;
|
|
782
|
-
}
|
|
783
|
-
async function loadWorkflowsMetadata(discovered) {
|
|
784
|
-
const results = await Promise.all(discovered.map(async (wf) => {
|
|
785
|
-
const loaded = await WorkflowLoader.loadWorkflow(wf);
|
|
786
|
-
if (!loaded.ok)
|
|
787
|
-
return null;
|
|
788
|
-
return {
|
|
789
|
-
...wf,
|
|
790
|
-
description: loaded.value.definition.description,
|
|
791
|
-
inputs: loaded.value.definition.inputs
|
|
792
|
-
};
|
|
793
|
-
}));
|
|
794
|
-
return results.filter((r) => r !== null);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
// src/sdk/runtime/executor.ts
|
|
798
|
-
import { join as join3, resolve } from "path";
|
|
799
|
-
import { homedir as homedir2 } from "os";
|
|
800
|
-
import { mkdir, writeFile as writeFile2, readFile } from "fs/promises";
|
|
801
|
-
|
|
802
|
-
// src/sdk/components/orchestrator-panel.tsx
|
|
803
|
-
import { createCliRenderer } from "@opentui/core";
|
|
804
|
-
import { createRoot } from "@opentui/react";
|
|
805
|
-
|
|
806
|
-
// src/sdk/components/orchestrator-panel-store.ts
|
|
807
|
-
class PanelStore {
|
|
808
|
-
version = 0;
|
|
809
|
-
workflowName = "";
|
|
810
|
-
agent = "";
|
|
811
|
-
prompt = "";
|
|
812
|
-
sessions = [];
|
|
813
|
-
completionInfo = null;
|
|
814
|
-
fatalError = null;
|
|
815
|
-
completionReached = false;
|
|
816
|
-
exitResolve = null;
|
|
817
|
-
abortResolve = null;
|
|
818
|
-
listeners = new Set;
|
|
819
|
-
subscribe = (fn) => {
|
|
820
|
-
this.listeners.add(fn);
|
|
821
|
-
return () => this.listeners.delete(fn);
|
|
822
|
-
};
|
|
823
|
-
emit() {
|
|
824
|
-
this.version++;
|
|
825
|
-
for (const fn of this.listeners)
|
|
826
|
-
fn();
|
|
827
|
-
}
|
|
828
|
-
setWorkflowInfo(name, agent, sessions, prompt) {
|
|
829
|
-
this.workflowName = name;
|
|
830
|
-
this.agent = agent;
|
|
831
|
-
this.prompt = prompt;
|
|
832
|
-
this.sessions = [
|
|
833
|
-
{
|
|
834
|
-
name: "orchestrator",
|
|
835
|
-
status: "running",
|
|
836
|
-
parents: [],
|
|
837
|
-
startedAt: Date.now(),
|
|
838
|
-
endedAt: null
|
|
839
|
-
},
|
|
840
|
-
...sessions.map((s) => ({
|
|
841
|
-
name: s.name,
|
|
842
|
-
status: "pending",
|
|
843
|
-
parents: s.parents.length > 0 ? s.parents : ["orchestrator"],
|
|
844
|
-
startedAt: null,
|
|
845
|
-
endedAt: null
|
|
846
|
-
}))
|
|
847
|
-
];
|
|
848
|
-
this.emit();
|
|
849
|
-
}
|
|
850
|
-
startSession(name) {
|
|
851
|
-
const session = this.sessions.find((s) => s.name === name);
|
|
852
|
-
if (!session)
|
|
853
|
-
return;
|
|
854
|
-
session.status = "running";
|
|
855
|
-
session.startedAt = Date.now();
|
|
856
|
-
this.emit();
|
|
857
|
-
}
|
|
858
|
-
completeSession(name) {
|
|
859
|
-
const session = this.sessions.find((s) => s.name === name);
|
|
860
|
-
if (!session)
|
|
861
|
-
return;
|
|
862
|
-
session.status = "complete";
|
|
863
|
-
session.endedAt = Date.now();
|
|
864
|
-
this.emit();
|
|
865
|
-
}
|
|
866
|
-
failSession(name, error) {
|
|
867
|
-
const session = this.sessions.find((s) => s.name === name);
|
|
868
|
-
if (!session)
|
|
869
|
-
return;
|
|
870
|
-
session.status = "error";
|
|
871
|
-
session.error = error;
|
|
872
|
-
session.endedAt = Date.now();
|
|
873
|
-
this.emit();
|
|
874
|
-
}
|
|
875
|
-
addSession(session) {
|
|
876
|
-
this.sessions.push(session);
|
|
877
|
-
this.emit();
|
|
878
|
-
}
|
|
879
|
-
setCompletion(workflowName, transcriptsPath) {
|
|
880
|
-
this.completionInfo = { workflowName, transcriptsPath };
|
|
881
|
-
const orch = this.sessions.find((s) => s.name === "orchestrator");
|
|
882
|
-
if (orch) {
|
|
883
|
-
orch.status = "complete";
|
|
884
|
-
orch.endedAt = Date.now();
|
|
885
|
-
}
|
|
886
|
-
this.emit();
|
|
887
|
-
}
|
|
888
|
-
setFatalError(message) {
|
|
889
|
-
this.fatalError = message;
|
|
890
|
-
this.completionReached = true;
|
|
891
|
-
const orch = this.sessions.find((s) => s.name === "orchestrator");
|
|
892
|
-
if (orch) {
|
|
893
|
-
orch.status = "error";
|
|
894
|
-
orch.endedAt = Date.now();
|
|
895
|
-
}
|
|
896
|
-
this.emit();
|
|
897
|
-
}
|
|
898
|
-
resolveExit() {
|
|
899
|
-
if (this.exitResolve) {
|
|
900
|
-
const resolve = this.exitResolve;
|
|
901
|
-
this.exitResolve = null;
|
|
902
|
-
resolve();
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
resolveAbort() {
|
|
906
|
-
if (this.abortResolve) {
|
|
907
|
-
const resolve = this.abortResolve;
|
|
908
|
-
this.abortResolve = null;
|
|
909
|
-
resolve();
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
requestQuit() {
|
|
913
|
-
if (this.completionReached) {
|
|
914
|
-
this.resolveExit();
|
|
915
|
-
} else {
|
|
916
|
-
this.resolveAbort();
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
markCompletionReached() {
|
|
920
|
-
this.completionReached = true;
|
|
921
|
-
this.emit();
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
// src/sdk/components/orchestrator-panel-contexts.ts
|
|
926
|
-
import { createContext, useContext, useSyncExternalStore } from "react";
|
|
927
|
-
var StoreContext = createContext(null);
|
|
928
|
-
var ThemeContext = createContext(null);
|
|
929
|
-
var TmuxSessionContext = createContext("");
|
|
930
|
-
|
|
931
|
-
// src/sdk/components/session-graph-panel.tsx
|
|
932
|
-
import {
|
|
933
|
-
useKeyboard,
|
|
934
|
-
useTerminalDimensions,
|
|
935
|
-
useRenderer
|
|
936
|
-
} from "@opentui/react";
|
|
937
|
-
import {
|
|
938
|
-
useState,
|
|
939
|
-
useEffect,
|
|
940
|
-
useMemo,
|
|
941
|
-
useCallback,
|
|
942
|
-
useRef,
|
|
943
|
-
useContext as useContext2
|
|
944
|
-
} from "react";
|
|
945
|
-
|
|
946
|
-
// src/sdk/components/node-card.tsx
|
|
947
|
-
import { jsxDEV } from "@opentui/react/jsx-dev-runtime";
|
|
948
|
-
|
|
949
|
-
// src/sdk/components/edge.tsx
|
|
950
|
-
import { jsxDEV as jsxDEV2 } from "@opentui/react/jsx-dev-runtime";
|
|
951
|
-
|
|
952
|
-
// src/sdk/components/header.tsx
|
|
953
|
-
import { jsxDEV as jsxDEV3 } from "@opentui/react/jsx-dev-runtime";
|
|
954
|
-
|
|
955
|
-
// src/sdk/components/statusline.tsx
|
|
956
|
-
import { jsxDEV as jsxDEV4 } from "@opentui/react/jsx-dev-runtime";
|
|
957
|
-
|
|
958
|
-
// src/sdk/components/session-graph-panel.tsx
|
|
959
|
-
import { jsxDEV as jsxDEV5 } from "@opentui/react/jsx-dev-runtime";
|
|
960
|
-
|
|
961
|
-
// src/sdk/components/error-boundary.tsx
|
|
962
|
-
import { Component } from "react";
|
|
963
|
-
|
|
964
|
-
// src/sdk/components/orchestrator-panel.tsx
|
|
965
|
-
import { jsxDEV as jsxDEV6 } from "@opentui/react/jsx-dev-runtime";
|
|
966
|
-
// src/sdk/runtime/executor.ts
|
|
967
|
-
function generateId() {
|
|
968
|
-
return crypto.randomUUID().slice(0, 8);
|
|
969
|
-
}
|
|
970
|
-
function getSessionsBaseDir() {
|
|
971
|
-
return join3(homedir2(), ".atomic", "sessions");
|
|
972
|
-
}
|
|
973
|
-
async function ensureDir(dir) {
|
|
974
|
-
await mkdir(dir, { recursive: true });
|
|
975
|
-
}
|
|
976
|
-
function escBash(s) {
|
|
977
|
-
return s.replace(/\x00/g, "").replace(/[\n\r]+/g, " ").replace(/[\\"$`!]/g, "\\$&");
|
|
978
|
-
}
|
|
979
|
-
function escPwsh(s) {
|
|
980
|
-
return s.replace(/\x00/g, "").replace(/[`"$]/g, "`$&").replace(/\n/g, "`n").replace(/\r/g, "`r");
|
|
981
|
-
}
|
|
982
|
-
async function executeWorkflow(options) {
|
|
983
|
-
const {
|
|
984
|
-
definition,
|
|
985
|
-
agent,
|
|
986
|
-
inputs = {},
|
|
987
|
-
workflowFile,
|
|
988
|
-
projectRoot = process.cwd()
|
|
989
|
-
} = options;
|
|
990
|
-
const workflowRunId = generateId();
|
|
991
|
-
const tmuxSessionName = `atomic-wf-${definition.name}-${workflowRunId}`;
|
|
992
|
-
const sessionsBaseDir = join3(getSessionsBaseDir(), workflowRunId);
|
|
993
|
-
await ensureDir(sessionsBaseDir);
|
|
994
|
-
const thisFile = resolve(import.meta.dir, "executor-entry.ts");
|
|
995
|
-
const isWin = process.platform === "win32";
|
|
996
|
-
const launcherExt = isWin ? "ps1" : "sh";
|
|
997
|
-
const launcherPath = join3(sessionsBaseDir, `orchestrator.${launcherExt}`);
|
|
998
|
-
const logPath = join3(sessionsBaseDir, "orchestrator.log");
|
|
999
|
-
const inputsB64 = Buffer.from(JSON.stringify(inputs)).toString("base64");
|
|
1000
|
-
const launcherScript = isWin ? [
|
|
1001
|
-
`Set-Location "${escPwsh(projectRoot)}"`,
|
|
1002
|
-
`$env:ATOMIC_WF_ID = "${escPwsh(workflowRunId)}"`,
|
|
1003
|
-
`$env:ATOMIC_WF_TMUX = "${escPwsh(tmuxSessionName)}"`,
|
|
1004
|
-
`$env:ATOMIC_WF_AGENT = "${escPwsh(agent)}"`,
|
|
1005
|
-
`$env:ATOMIC_WF_INPUTS = "${escPwsh(inputsB64)}"`,
|
|
1006
|
-
`$env:ATOMIC_WF_FILE = "${escPwsh(workflowFile)}"`,
|
|
1007
|
-
`$env:ATOMIC_WF_CWD = "${escPwsh(projectRoot)}"`,
|
|
1008
|
-
`bun run "${escPwsh(thisFile)}" 2>"${escPwsh(logPath)}"`
|
|
1009
|
-
].join(`
|
|
1010
|
-
`) : [
|
|
1011
|
-
"#!/bin/bash",
|
|
1012
|
-
`cd "${escBash(projectRoot)}"`,
|
|
1013
|
-
`export ATOMIC_WF_ID="${escBash(workflowRunId)}"`,
|
|
1014
|
-
`export ATOMIC_WF_TMUX="${escBash(tmuxSessionName)}"`,
|
|
1015
|
-
`export ATOMIC_WF_AGENT="${escBash(agent)}"`,
|
|
1016
|
-
`export ATOMIC_WF_INPUTS="${escBash(inputsB64)}"`,
|
|
1017
|
-
`export ATOMIC_WF_FILE="${escBash(workflowFile)}"`,
|
|
1018
|
-
`export ATOMIC_WF_CWD="${escBash(projectRoot)}"`,
|
|
1019
|
-
`bun run "${escBash(thisFile)}" 2>"${escBash(logPath)}"`
|
|
1020
|
-
].join(`
|
|
1021
|
-
`);
|
|
1022
|
-
await writeFile2(launcherPath, launcherScript, { mode: 493 });
|
|
1023
|
-
console.log(`[atomic] Session: ${tmuxSessionName} (FYI all atomic sessions run on tmux -L ${SOCKET_NAME})`);
|
|
1024
|
-
if (isInsideTmux()) {
|
|
1025
|
-
const defaultShell = process.env.SHELL || (isWin ? "pwsh" : "sh");
|
|
1026
|
-
createSession(tmuxSessionName, defaultShell, "orchestrator");
|
|
1027
|
-
const launcherCmd = isWin ? ["pwsh", "-NoProfile", "-File", launcherPath] : ["bash", launcherPath];
|
|
1028
|
-
const proc = Bun.spawn(launcherCmd, {
|
|
1029
|
-
stdio: ["inherit", "inherit", "inherit"],
|
|
1030
|
-
cwd: projectRoot
|
|
1031
|
-
});
|
|
1032
|
-
await proc.exited;
|
|
1033
|
-
} else {
|
|
1034
|
-
const shellCmd = isWin ? `pwsh -NoProfile -File "${escPwsh(launcherPath)}"` : `bash "${escBash(launcherPath)}"`;
|
|
1035
|
-
createSession(tmuxSessionName, shellCmd, "orchestrator");
|
|
1036
|
-
const attachProc = spawnMuxAttach(tmuxSessionName);
|
|
1037
|
-
await attachProc.exited;
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
export { WorkflowBuilder, defineWorkflow, validateCopilotWorkflow, validateOpenCodeWorkflow, SOCKET_NAME, getMuxBinary, resetMuxBinaryCache, isTmuxInstalled, isInsideTmux, tmuxRun, createSession, createWindow, createPane, sendLiteralText, sendSpecialKey, sendKeysAndSubmit, capturePane, capturePaneVisible, capturePaneScrollback, killSession, killWindow, sessionExists, attachSession, spawnMuxAttach, switchClient, getCurrentSession, attachOrSwitch, selectWindow, normalizeTmuxCapture, normalizeTmuxLines, paneLooksReady, paneHasActiveTask, paneIsIdle, waitForPaneReady, attemptSubmitRounds, waitForOutput, clearClaudeSession, createClaudeSession, claudeQuery, validateClaudeWorkflow, WorkflowLoader, AGENTS, WORKFLOWS_GITIGNORE, discoverWorkflows, findWorkflow, loadWorkflowsMetadata, executeWorkflow };
|