forge-remote 0.1.11 → 0.1.14
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/package.json +1 -1
- package/src/init.js +1 -1
- package/src/session-manager.js +76 -40
package/package.json
CHANGED
package/src/init.js
CHANGED
|
@@ -1039,7 +1039,7 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
|
|
|
1039
1039
|
|
|
1040
1040
|
console.log(
|
|
1041
1041
|
chalk.dim(
|
|
1042
|
-
" Love Forge Remote? Support us: https://
|
|
1042
|
+
" Love Forge Remote? Support us: https://buy.stripe.com/7sY5kDcL7e792rs06E5J606\n",
|
|
1043
1043
|
),
|
|
1044
1044
|
);
|
|
1045
1045
|
}
|
package/src/session-manager.js
CHANGED
|
@@ -597,6 +597,7 @@ async function sendFollowUpPrompt(sessionId, prompt) {
|
|
|
597
597
|
session.permissionWatcher = null;
|
|
598
598
|
}
|
|
599
599
|
session.permissionNeeded = false;
|
|
600
|
+
session.deniedToolCall = null;
|
|
600
601
|
|
|
601
602
|
// Store the user's message in Firestore.
|
|
602
603
|
const db = getDb();
|
|
@@ -625,6 +626,7 @@ async function runClaudeProcess(sessionId, prompt) {
|
|
|
625
626
|
|
|
626
627
|
// Reset permission state for this new turn.
|
|
627
628
|
session.permissionNeeded = false;
|
|
629
|
+
session.deniedToolCall = null;
|
|
628
630
|
session.lastToolCall = null;
|
|
629
631
|
|
|
630
632
|
const db = getDb();
|
|
@@ -719,7 +721,11 @@ async function runClaudeProcess(sessionId, prompt) {
|
|
|
719
721
|
// ── Parse stdout (stream-json) ──
|
|
720
722
|
let stdoutBuffer = "";
|
|
721
723
|
|
|
722
|
-
|
|
724
|
+
// Track pending async data processing so the close handler can wait for it.
|
|
725
|
+
// Without this, the close event can fire before permission detection completes.
|
|
726
|
+
let pendingDataProcessing = Promise.resolve();
|
|
727
|
+
|
|
728
|
+
claudeProcess.stdout.on("data", (data) => {
|
|
723
729
|
if (!receivedOutput) {
|
|
724
730
|
receivedOutput = true;
|
|
725
731
|
clearTimeout(watchdog);
|
|
@@ -727,40 +733,50 @@ async function runClaudeProcess(sessionId, prompt) {
|
|
|
727
733
|
log.session(sessionId, "First output received from Claude");
|
|
728
734
|
}
|
|
729
735
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
736
|
+
// Chain onto the pending processing promise to ensure sequential handling
|
|
737
|
+
// AND to let the close handler wait for all processing to finish.
|
|
738
|
+
pendingDataProcessing = pendingDataProcessing
|
|
739
|
+
.then(async () => {
|
|
740
|
+
stdoutBuffer += data.toString();
|
|
741
|
+
const lines = stdoutBuffer.split("\n");
|
|
742
|
+
stdoutBuffer = lines.pop();
|
|
743
|
+
|
|
744
|
+
for (const line of lines) {
|
|
745
|
+
if (!line.trim()) continue;
|
|
746
|
+
try {
|
|
747
|
+
const event = JSON.parse(line);
|
|
748
|
+
await handleStreamEvent(sessionId, sessionRef, event);
|
|
749
|
+
} catch {
|
|
750
|
+
if (line.trim()) {
|
|
751
|
+
const text = line.trim();
|
|
752
|
+
log.claudeOutput(sessionId, text);
|
|
753
|
+
|
|
754
|
+
// Detect auth / login errors that come as plain text.
|
|
755
|
+
if (/not logged in|please run.*login/i.test(text)) {
|
|
756
|
+
log.error(
|
|
757
|
+
`[${sessionId.slice(0, 8)}] Claude CLI auth error: ${text}`,
|
|
758
|
+
);
|
|
759
|
+
await db
|
|
760
|
+
.collection("sessions")
|
|
761
|
+
.doc(sessionId)
|
|
762
|
+
.collection("messages")
|
|
763
|
+
.add({
|
|
764
|
+
type: "system",
|
|
765
|
+
content: `Auth error: ${text}. Run "claude /login" in your terminal, then restart the relay.`,
|
|
766
|
+
timestamp: FieldValue.serverTimestamp(),
|
|
767
|
+
});
|
|
768
|
+
} else {
|
|
769
|
+
await storeAssistantMessage(sessionId, text);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
760
772
|
}
|
|
761
773
|
}
|
|
762
|
-
}
|
|
763
|
-
|
|
774
|
+
})
|
|
775
|
+
.catch((err) => {
|
|
776
|
+
log.error(
|
|
777
|
+
`[${sessionId.slice(0, 8)}] Error processing stdout: ${err.message}`,
|
|
778
|
+
);
|
|
779
|
+
});
|
|
764
780
|
});
|
|
765
781
|
|
|
766
782
|
// ── Stderr ──
|
|
@@ -800,6 +816,12 @@ async function runClaudeProcess(sessionId, prompt) {
|
|
|
800
816
|
claudeProcess.on("close", async (code, signal) => {
|
|
801
817
|
clearTimeout(watchdog);
|
|
802
818
|
clearTimeout(killTimer);
|
|
819
|
+
|
|
820
|
+
// Wait for all pending stdout data processing to complete before checking
|
|
821
|
+
// permission state. Without this, the close event can race ahead of the
|
|
822
|
+
// async data handler, causing permissionNeeded to still be false.
|
|
823
|
+
await pendingDataProcessing;
|
|
824
|
+
|
|
803
825
|
log.session(
|
|
804
826
|
sessionId,
|
|
805
827
|
`Process exited — code: ${code}, signal: ${signal}, PID: ${claudeProcess.pid}`,
|
|
@@ -826,11 +848,14 @@ async function runClaudeProcess(sessionId, prompt) {
|
|
|
826
848
|
if (sess) sess.process = null; // Clear process so follow-ups can spawn.
|
|
827
849
|
|
|
828
850
|
if (code === 0) {
|
|
829
|
-
|
|
851
|
+
// Use deniedToolCall (frozen at detection time) so subsequent tool calls
|
|
852
|
+
// don't overwrite the permission target. Fall back to lastToolCall.
|
|
853
|
+
const toolForPermission = sess?.deniedToolCall || sess?.lastToolCall;
|
|
854
|
+
if (sess?.permissionNeeded && toolForPermission) {
|
|
830
855
|
// Claude was denied a tool call — create a permission request and wait.
|
|
831
856
|
const permDocId = await createPermissionRequest(
|
|
832
857
|
sessionId,
|
|
833
|
-
|
|
858
|
+
toolForPermission,
|
|
834
859
|
);
|
|
835
860
|
await sessionRef.update({
|
|
836
861
|
status: "waiting_permission",
|
|
@@ -1218,15 +1243,21 @@ async function handleStreamEvent(sessionId, sessionRef, event) {
|
|
|
1218
1243
|
|
|
1219
1244
|
// Patterns that indicate Claude was denied permission for a tool call.
|
|
1220
1245
|
const PERMISSION_PATTERNS = [
|
|
1221
|
-
/need your approval/i,
|
|
1222
|
-
/
|
|
1223
|
-
/
|
|
1246
|
+
/need(?:s|ing)? your approval/i,
|
|
1247
|
+
/awaiting your approval/i,
|
|
1248
|
+
/permission (?:required|denied|blocked|pending)/i,
|
|
1249
|
+
/could you.{0,10}approve/i,
|
|
1250
|
+
/please.{0,10}approve/i,
|
|
1224
1251
|
/keeps? getting blocked/i,
|
|
1225
1252
|
/permission system/i,
|
|
1226
1253
|
/being blocked by/i,
|
|
1227
|
-
/approve the command/i,
|
|
1254
|
+
/approve the (?:command|tool|action|edit)/i,
|
|
1228
1255
|
/blocked by.+permission/i,
|
|
1229
1256
|
/can'?t (?:execute|run).+(?:permission|approval)/i,
|
|
1257
|
+
/stuck on permission/i,
|
|
1258
|
+
/permission.{0,20}(?:is |still )?pending/i,
|
|
1259
|
+
/waiting for.{0,15}(?:permission|approval)/i,
|
|
1260
|
+
/need(?:s|ed)? (?:to be )?approv/i,
|
|
1230
1261
|
];
|
|
1231
1262
|
|
|
1232
1263
|
async function storeAssistantMessage(sessionId, text) {
|
|
@@ -1241,10 +1272,13 @@ async function storeAssistantMessage(sessionId, text) {
|
|
|
1241
1272
|
});
|
|
1242
1273
|
|
|
1243
1274
|
// Check if Claude's message indicates a permission denial.
|
|
1275
|
+
// Save the denied tool call separately so subsequent tool calls don't
|
|
1276
|
+
// overwrite it — the permission request must be for the denied tool.
|
|
1244
1277
|
if (session?.lastToolCall && !session.permissionNeeded) {
|
|
1245
1278
|
for (const pattern of PERMISSION_PATTERNS) {
|
|
1246
1279
|
if (pattern.test(text)) {
|
|
1247
1280
|
session.permissionNeeded = true;
|
|
1281
|
+
session.deniedToolCall = { ...session.lastToolCall };
|
|
1248
1282
|
log.info(
|
|
1249
1283
|
`[${sessionId.slice(0, 8)}] Permission denial detected for ${session.lastToolCall.name}`,
|
|
1250
1284
|
);
|
|
@@ -1817,8 +1851,9 @@ async function handlePermissionApproved(sessionId, permData) {
|
|
|
1817
1851
|
const session = activeSessions.get(sessionId);
|
|
1818
1852
|
if (!session) return;
|
|
1819
1853
|
|
|
1820
|
-
const toolCall = session.lastToolCall;
|
|
1854
|
+
const toolCall = session.deniedToolCall || session.lastToolCall;
|
|
1821
1855
|
session.permissionNeeded = false;
|
|
1856
|
+
session.deniedToolCall = null;
|
|
1822
1857
|
|
|
1823
1858
|
log.success(
|
|
1824
1859
|
`[${sessionId.slice(0, 8)}] Permission approved for ${permData.toolName}`,
|
|
@@ -1863,6 +1898,7 @@ async function handlePermissionDenied(sessionId) {
|
|
|
1863
1898
|
if (!session) return;
|
|
1864
1899
|
|
|
1865
1900
|
session.permissionNeeded = false;
|
|
1901
|
+
session.deniedToolCall = null;
|
|
1866
1902
|
session.lastToolCall = null;
|
|
1867
1903
|
|
|
1868
1904
|
log.info(`[${sessionId.slice(0, 8)}] Permission denied`);
|