@electric-agent/studio 1.3.4 → 1.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/dist/bridge/claude-code-docker.d.ts.map +1 -1
- package/dist/bridge/claude-code-docker.js +13 -28
- package/dist/bridge/claude-code-docker.js.map +1 -1
- package/dist/bridge/claude-code-sprites.d.ts.map +1 -1
- package/dist/bridge/claude-code-sprites.js +13 -27
- package/dist/bridge/claude-code-sprites.js.map +1 -1
- package/dist/bridge/gate-response.d.ts +10 -0
- package/dist/bridge/gate-response.d.ts.map +1 -0
- package/dist/bridge/gate-response.js +40 -0
- package/dist/bridge/gate-response.js.map +1 -0
- package/dist/bridge/index.d.ts +1 -0
- package/dist/bridge/index.d.ts.map +1 -1
- package/dist/bridge/index.js +1 -0
- package/dist/bridge/index.js.map +1 -1
- package/dist/bridge/stream-json-parser.js +1 -0
- package/dist/bridge/stream-json-parser.js.map +1 -1
- package/dist/client/assets/index-DDzmxYub.js +234 -0
- package/dist/client/assets/index-DcP7prsZ.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/git.d.ts +8 -5
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +8 -5
- package/dist/git.js.map +1 -1
- package/dist/sandbox/docker.d.ts.map +1 -1
- package/dist/sandbox/docker.js +1 -0
- package/dist/sandbox/docker.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +183 -37
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/client/assets/index-B6arNdVE.css +0 -1
- package/dist/client/assets/index-CxBu-PUg.js +0 -234
package/dist/server.js
CHANGED
|
@@ -164,6 +164,7 @@ function mapHookToEngineEvent(body) {
|
|
|
164
164
|
tool_use_id: toolUseId,
|
|
165
165
|
question: firstQuestion?.question || toolInput.question || "",
|
|
166
166
|
options: firstQuestion?.options,
|
|
167
|
+
questions: questions ?? undefined,
|
|
167
168
|
ts: now,
|
|
168
169
|
};
|
|
169
170
|
}
|
|
@@ -432,7 +433,7 @@ export function createApp(config) {
|
|
|
432
433
|
console.log(`[hook-event] Blocking for ask_user_question gate: ${toolUseId}`);
|
|
433
434
|
try {
|
|
434
435
|
const gateTimeout = 5 * 60 * 1000; // 5 minutes
|
|
435
|
-
const
|
|
436
|
+
const result = await Promise.race([
|
|
436
437
|
createGate(sessionId, `ask_user_question:${toolUseId}`),
|
|
437
438
|
new Promise((_, reject) => setTimeout(() => reject(new Error("AskUserQuestion gate timed out")), gateTimeout)),
|
|
438
439
|
]);
|
|
@@ -443,7 +444,7 @@ export function createApp(config) {
|
|
|
443
444
|
permissionDecision: "allow",
|
|
444
445
|
updatedInput: {
|
|
445
446
|
questions: body.tool_input?.questions,
|
|
446
|
-
answers:
|
|
447
|
+
answers: result.answers,
|
|
447
448
|
},
|
|
448
449
|
},
|
|
449
450
|
});
|
|
@@ -562,7 +563,7 @@ export function createApp(config) {
|
|
|
562
563
|
console.log(`[hook] Blocking for ask_user_question gate: ${toolUseId}`);
|
|
563
564
|
try {
|
|
564
565
|
const gateTimeout = 5 * 60 * 1000;
|
|
565
|
-
const
|
|
566
|
+
const result = await Promise.race([
|
|
566
567
|
createGate(sessionId, `ask_user_question:${toolUseId}`),
|
|
567
568
|
new Promise((_, reject) => setTimeout(() => reject(new Error("AskUserQuestion gate timed out")), gateTimeout)),
|
|
568
569
|
]);
|
|
@@ -574,7 +575,7 @@ export function createApp(config) {
|
|
|
574
575
|
permissionDecision: "allow",
|
|
575
576
|
updatedInput: {
|
|
576
577
|
questions: body.tool_input?.questions,
|
|
577
|
-
answers:
|
|
578
|
+
answers: result.answers,
|
|
578
579
|
},
|
|
579
580
|
},
|
|
580
581
|
});
|
|
@@ -717,8 +718,9 @@ echo "Start claude in this project — the session will appear in the studio UI.
|
|
|
717
718
|
// Write user prompt to the stream so it shows in the UI
|
|
718
719
|
await bridge.emit({ type: "user_prompt", message: body.description, ts: ts() });
|
|
719
720
|
// Gather GitHub accounts for the merged setup gate
|
|
721
|
+
// Only check if the client provided a token — never fall back to server-side GH_TOKEN
|
|
720
722
|
let ghAccounts = [];
|
|
721
|
-
if (isGhAuthenticated(body.ghToken)) {
|
|
723
|
+
if (body.ghToken && isGhAuthenticated(body.ghToken)) {
|
|
722
724
|
try {
|
|
723
725
|
ghAccounts = ghListAccounts(body.ghToken);
|
|
724
726
|
}
|
|
@@ -932,14 +934,16 @@ echo "Start claude in this project — the session will appear in the studio UI.
|
|
|
932
934
|
}
|
|
933
935
|
config.sessions.update(sessionId, updates);
|
|
934
936
|
// Check if the app is running after completion
|
|
935
|
-
// and emit
|
|
937
|
+
// and emit app_status so the UI shows the preview link
|
|
936
938
|
if (success) {
|
|
937
939
|
try {
|
|
938
940
|
const appRunning = await config.sandbox.isAppRunning(handle);
|
|
939
941
|
if (appRunning) {
|
|
940
942
|
await bridge.emit({
|
|
941
|
-
type: "
|
|
943
|
+
type: "app_status",
|
|
944
|
+
status: "running",
|
|
942
945
|
port: handle.port ?? session.appPort,
|
|
946
|
+
previewUrl: handle.previewUrl ?? session.previewUrl,
|
|
943
947
|
ts: ts(),
|
|
944
948
|
});
|
|
945
949
|
}
|
|
@@ -1022,7 +1026,13 @@ echo "Start claude in this project — the session will appear in the studio UI.
|
|
|
1022
1026
|
message: "App started",
|
|
1023
1027
|
ts: ts(),
|
|
1024
1028
|
});
|
|
1025
|
-
await bridge.emit({
|
|
1029
|
+
await bridge.emit({
|
|
1030
|
+
type: "app_status",
|
|
1031
|
+
status: "running",
|
|
1032
|
+
port: session.appPort,
|
|
1033
|
+
previewUrl: session.previewUrl,
|
|
1034
|
+
ts: ts(),
|
|
1035
|
+
});
|
|
1026
1036
|
}
|
|
1027
1037
|
}
|
|
1028
1038
|
catch (err) {
|
|
@@ -1089,8 +1099,10 @@ echo "Start claude in this project — the session will appear in the studio UI.
|
|
|
1089
1099
|
if (!toolUseId) {
|
|
1090
1100
|
return c.json({ error: "toolUseId is required for ask_user_question" }, 400);
|
|
1091
1101
|
}
|
|
1092
|
-
|
|
1093
|
-
const
|
|
1102
|
+
// Accept either answers (Record<string, string>) or legacy answer (string)
|
|
1103
|
+
const answers = body.answers ??
|
|
1104
|
+
(body.answer ? { [body.question || "answer"]: body.answer } : {});
|
|
1105
|
+
const resolved = resolveGate(sessionId, `ask_user_question:${toolUseId}`, { answers });
|
|
1094
1106
|
if (resolved) {
|
|
1095
1107
|
// Hook session — gate was blocking, now resolved
|
|
1096
1108
|
try {
|
|
@@ -1719,9 +1731,11 @@ echo "Start claude in this project — the session will appear in the studio UI.
|
|
|
1719
1731
|
}
|
|
1720
1732
|
return c.json({ content });
|
|
1721
1733
|
});
|
|
1722
|
-
// List GitHub accounts (personal + orgs)
|
|
1734
|
+
// List GitHub accounts (personal + orgs) — requires client-provided token
|
|
1723
1735
|
app.get("/api/github/accounts", (c) => {
|
|
1724
|
-
const token = c.req.header("X-GH-Token")
|
|
1736
|
+
const token = c.req.header("X-GH-Token");
|
|
1737
|
+
if (!token)
|
|
1738
|
+
return c.json({ accounts: [] });
|
|
1725
1739
|
try {
|
|
1726
1740
|
const accounts = ghListAccounts(token);
|
|
1727
1741
|
return c.json({ accounts });
|
|
@@ -1730,9 +1744,11 @@ echo "Start claude in this project — the session will appear in the studio UI.
|
|
|
1730
1744
|
return c.json({ error: e instanceof Error ? e.message : "Failed to list accounts" }, 500);
|
|
1731
1745
|
}
|
|
1732
1746
|
});
|
|
1733
|
-
// List GitHub repos for the authenticated user
|
|
1747
|
+
// List GitHub repos for the authenticated user — requires client-provided token
|
|
1734
1748
|
app.get("/api/github/repos", (c) => {
|
|
1735
|
-
const token = c.req.header("X-GH-Token")
|
|
1749
|
+
const token = c.req.header("X-GH-Token");
|
|
1750
|
+
if (!token)
|
|
1751
|
+
return c.json({ repos: [] });
|
|
1736
1752
|
try {
|
|
1737
1753
|
const repos = ghListRepos(50, token);
|
|
1738
1754
|
return c.json({ repos });
|
|
@@ -1744,7 +1760,9 @@ echo "Start claude in this project — the session will appear in the studio UI.
|
|
|
1744
1760
|
app.get("/api/github/repos/:owner/:repo/branches", (c) => {
|
|
1745
1761
|
const owner = c.req.param("owner");
|
|
1746
1762
|
const repo = c.req.param("repo");
|
|
1747
|
-
const token = c.req.header("X-GH-Token")
|
|
1763
|
+
const token = c.req.header("X-GH-Token");
|
|
1764
|
+
if (!token)
|
|
1765
|
+
return c.json({ branches: [] });
|
|
1748
1766
|
try {
|
|
1749
1767
|
const branches = ghListBranches(`${owner}/${repo}`, token);
|
|
1750
1768
|
return c.json({ branches });
|
|
@@ -1797,24 +1815,47 @@ echo "Start claude in this project — the session will appear in the studio UI.
|
|
|
1797
1815
|
catch {
|
|
1798
1816
|
return c.json({ error: "Failed to create event stream" }, 500);
|
|
1799
1817
|
}
|
|
1800
|
-
|
|
1818
|
+
// Create the initial session bridge for emitting progress events
|
|
1819
|
+
const bridge = getOrCreateBridge(config, sessionId);
|
|
1820
|
+
// Record session as running (like normal session creation)
|
|
1821
|
+
const sandboxProjectDir = `/home/agent/workspace/${repoName}`;
|
|
1822
|
+
const session = {
|
|
1823
|
+
id: sessionId,
|
|
1824
|
+
projectName: body.branch ? `${repoName}/${body.branch}` : repoName,
|
|
1825
|
+
sandboxProjectDir,
|
|
1826
|
+
description: `Resumed from ${body.repoUrl}`,
|
|
1827
|
+
createdAt: new Date().toISOString(),
|
|
1828
|
+
lastActiveAt: new Date().toISOString(),
|
|
1829
|
+
status: "running",
|
|
1830
|
+
};
|
|
1831
|
+
config.sessions.add(session);
|
|
1832
|
+
// Write user prompt to the stream so it shows in the UI
|
|
1833
|
+
await bridge.emit({
|
|
1834
|
+
type: "user_prompt",
|
|
1835
|
+
message: `Resume from ${body.repoUrl}`,
|
|
1836
|
+
ts: ts(),
|
|
1837
|
+
});
|
|
1838
|
+
// Launch async flow: clone repo → set up Claude Code → start exploring
|
|
1839
|
+
const asyncFlow = async () => {
|
|
1840
|
+
// 1. Clone the repo into a sandbox
|
|
1841
|
+
await bridge.emit({
|
|
1842
|
+
type: "log",
|
|
1843
|
+
level: "build",
|
|
1844
|
+
message: "Cloning repository...",
|
|
1845
|
+
ts: ts(),
|
|
1846
|
+
});
|
|
1801
1847
|
const handle = await config.sandbox.createFromRepo(sessionId, body.repoUrl, {
|
|
1802
1848
|
branch: body.branch,
|
|
1803
1849
|
apiKey: body.apiKey,
|
|
1804
1850
|
oauthToken: body.oauthToken,
|
|
1805
1851
|
ghToken: body.ghToken,
|
|
1806
1852
|
});
|
|
1807
|
-
// Get git state from cloned repo
|
|
1853
|
+
// Get git state from cloned repo
|
|
1808
1854
|
const gs = await config.sandbox.gitStatus(handle, handle.projectDir);
|
|
1809
|
-
|
|
1810
|
-
id: sessionId,
|
|
1811
|
-
projectName: repoName,
|
|
1812
|
-
sandboxProjectDir: handle.projectDir,
|
|
1813
|
-
description: `Resumed from ${body.repoUrl}`,
|
|
1814
|
-
createdAt: new Date().toISOString(),
|
|
1815
|
-
lastActiveAt: new Date().toISOString(),
|
|
1816
|
-
status: "complete",
|
|
1855
|
+
config.sessions.update(sessionId, {
|
|
1817
1856
|
appPort: handle.port,
|
|
1857
|
+
sandboxProjectDir: handle.projectDir,
|
|
1858
|
+
previewUrl: handle.previewUrl,
|
|
1818
1859
|
git: {
|
|
1819
1860
|
branch: gs.branch ?? body.branch ?? "main",
|
|
1820
1861
|
remoteUrl: body.repoUrl,
|
|
@@ -1823,23 +1864,128 @@ echo "Start claude in this project — the session will appear in the studio UI.
|
|
|
1823
1864
|
lastCommitMessage: gs.lastCommitMessage ?? null,
|
|
1824
1865
|
lastCheckpointAt: null,
|
|
1825
1866
|
},
|
|
1826
|
-
};
|
|
1827
|
-
config.sessions.add(session);
|
|
1828
|
-
// Write initial message to stream
|
|
1829
|
-
const bridge = getOrCreateBridge(config, sessionId);
|
|
1867
|
+
});
|
|
1830
1868
|
await bridge.emit({
|
|
1831
1869
|
type: "log",
|
|
1832
1870
|
level: "done",
|
|
1833
|
-
message:
|
|
1871
|
+
message: "Repository cloned",
|
|
1834
1872
|
ts: ts(),
|
|
1835
1873
|
});
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1874
|
+
// 2. Write CLAUDE.md to the sandbox workspace
|
|
1875
|
+
const claudeMd = generateClaudeMd({
|
|
1876
|
+
description: `Resumed from ${body.repoUrl}`,
|
|
1877
|
+
projectName: repoName,
|
|
1878
|
+
projectDir: handle.projectDir,
|
|
1879
|
+
runtime: config.sandbox.runtime,
|
|
1880
|
+
});
|
|
1881
|
+
try {
|
|
1882
|
+
await config.sandbox.exec(handle, `cat > '${handle.projectDir}/CLAUDE.md' << 'CLAUDEMD_EOF'\n${claudeMd}\nCLAUDEMD_EOF`);
|
|
1883
|
+
}
|
|
1884
|
+
catch (err) {
|
|
1885
|
+
console.error(`[session:${sessionId}] Failed to write CLAUDE.md:`, err);
|
|
1886
|
+
}
|
|
1887
|
+
// Ensure the create-app skill is present in the project
|
|
1888
|
+
if (createAppSkillContent) {
|
|
1889
|
+
try {
|
|
1890
|
+
const skillDir = `${handle.projectDir}/.claude/skills/create-app`;
|
|
1891
|
+
const skillB64 = Buffer.from(createAppSkillContent).toString("base64");
|
|
1892
|
+
await config.sandbox.exec(handle, `mkdir -p '${skillDir}' && echo '${skillB64}' | base64 -d > '${skillDir}/SKILL.md'`);
|
|
1893
|
+
}
|
|
1894
|
+
catch (err) {
|
|
1895
|
+
console.error(`[session:${sessionId}] Failed to write create-app skill:`, err);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
// 3. Create Claude Code bridge with a resume prompt
|
|
1899
|
+
const resumePrompt = "You are resuming work on an existing project. Explore the codebase to understand its structure, then wait for instructions from the user.";
|
|
1900
|
+
const claudeConfig = config.sandbox.runtime === "sprites"
|
|
1901
|
+
? {
|
|
1902
|
+
prompt: resumePrompt,
|
|
1903
|
+
cwd: handle.projectDir,
|
|
1904
|
+
studioUrl: resolveStudioUrl(config.port),
|
|
1905
|
+
}
|
|
1906
|
+
: {
|
|
1907
|
+
prompt: resumePrompt,
|
|
1908
|
+
cwd: handle.projectDir,
|
|
1909
|
+
studioPort: config.port,
|
|
1910
|
+
};
|
|
1911
|
+
const ccBridge = createClaudeCodeBridge(config, sessionId, claudeConfig);
|
|
1912
|
+
// 4. Register event listeners (reuse pattern from normal flow)
|
|
1913
|
+
ccBridge.onAgentEvent((event) => {
|
|
1914
|
+
if (event.type === "session_start") {
|
|
1915
|
+
const ccSessionId = event.session_id;
|
|
1916
|
+
console.log(`[session:${sessionId}] Captured Claude Code session ID: ${ccSessionId}`);
|
|
1917
|
+
if (ccSessionId) {
|
|
1918
|
+
config.sessions.update(sessionId, { lastCoderSessionId: ccSessionId });
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
});
|
|
1922
|
+
ccBridge.onComplete(async (success) => {
|
|
1923
|
+
const updates = {
|
|
1924
|
+
status: success ? "complete" : "error",
|
|
1925
|
+
};
|
|
1926
|
+
try {
|
|
1927
|
+
const latestGs = await config.sandbox.gitStatus(handle, handle.projectDir);
|
|
1928
|
+
if (latestGs.initialized) {
|
|
1929
|
+
const existing = config.sessions.get(sessionId);
|
|
1930
|
+
updates.git = {
|
|
1931
|
+
branch: latestGs.branch ?? "main",
|
|
1932
|
+
remoteUrl: existing?.git?.remoteUrl ?? null,
|
|
1933
|
+
repoName: existing?.git?.repoName ?? null,
|
|
1934
|
+
repoVisibility: existing?.git?.repoVisibility,
|
|
1935
|
+
lastCommitHash: latestGs.lastCommitHash ?? null,
|
|
1936
|
+
lastCommitMessage: latestGs.lastCommitMessage ?? null,
|
|
1937
|
+
lastCheckpointAt: existing?.git?.lastCheckpointAt ?? null,
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
catch {
|
|
1942
|
+
// Container may already be stopped
|
|
1943
|
+
}
|
|
1944
|
+
config.sessions.update(sessionId, updates);
|
|
1945
|
+
// Check if the app is running after completion
|
|
1946
|
+
if (success) {
|
|
1947
|
+
try {
|
|
1948
|
+
const appRunning = await config.sandbox.isAppRunning(handle);
|
|
1949
|
+
if (appRunning) {
|
|
1950
|
+
await ccBridge.emit({
|
|
1951
|
+
type: "app_status",
|
|
1952
|
+
status: "running",
|
|
1953
|
+
port: handle.port ?? session.appPort,
|
|
1954
|
+
previewUrl: handle.previewUrl ?? session.previewUrl,
|
|
1955
|
+
ts: ts(),
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
catch {
|
|
1960
|
+
// Container may already be stopped
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
});
|
|
1964
|
+
// 5. Start the bridge and send command
|
|
1965
|
+
await ccBridge.emit({
|
|
1966
|
+
type: "log",
|
|
1967
|
+
level: "build",
|
|
1968
|
+
message: "Starting Claude Code...",
|
|
1969
|
+
ts: ts(),
|
|
1970
|
+
});
|
|
1971
|
+
console.log(`[session:${sessionId}] Starting bridge listener...`);
|
|
1972
|
+
await ccBridge.start();
|
|
1973
|
+
console.log(`[session:${sessionId}] Bridge started, sending 'new' command...`);
|
|
1974
|
+
const newCmd = {
|
|
1975
|
+
command: "new",
|
|
1976
|
+
description: resumePrompt,
|
|
1977
|
+
projectName: repoName,
|
|
1978
|
+
baseDir: "/home/agent/workspace",
|
|
1979
|
+
};
|
|
1980
|
+
await ccBridge.sendCommand(newCmd);
|
|
1981
|
+
console.log(`[session:${sessionId}] Command sent, waiting for agent...`);
|
|
1982
|
+
};
|
|
1983
|
+
asyncFlow().catch(async (err) => {
|
|
1984
|
+
console.error(`[session:${sessionId}] Resume flow failed:`, err);
|
|
1985
|
+
config.sessions.update(sessionId, { status: "error" });
|
|
1986
|
+
});
|
|
1987
|
+
const sessionToken = deriveSessionToken(config.streamConfig.secret, sessionId);
|
|
1988
|
+
return c.json({ sessionId, session, sessionToken }, 201);
|
|
1843
1989
|
});
|
|
1844
1990
|
// Serve static SPA files (if built)
|
|
1845
1991
|
const clientDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), "./client");
|