forge-remote 0.1.18 → 0.1.20

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-remote",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "Desktop relay for Forge Remote — monitor and control Claude Code sessions from your phone",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -3,7 +3,7 @@
3
3
  // Created by Daniel Wendel, CEO/Founder of Iron Forge Apps
4
4
 
5
5
  import { getDb, FieldValue } from "./firebase.js";
6
- import { readdir, realpath } from "node:fs/promises";
6
+ import { readdir, realpath, mkdir } from "node:fs/promises";
7
7
  import { homedir } from "node:os";
8
8
  import path from "node:path";
9
9
  import { spawn, execSync } from "child_process";
@@ -490,8 +490,14 @@ export async function startNewSession(desktopId, payload) {
490
490
  );
491
491
  }
492
492
 
493
- const { prompt, projectPath, model, webhookMeta, allowedTools } =
494
- payload || {};
493
+ const {
494
+ prompt,
495
+ projectPath,
496
+ model,
497
+ webhookMeta,
498
+ allowedTools,
499
+ createIfMissing,
500
+ } = payload || {};
495
501
  const db = getDb();
496
502
  const resolvedModel = model || "sonnet";
497
503
  const resolvedPath = projectPath || process.cwd();
@@ -503,7 +509,12 @@ export async function startNewSession(desktopId, payload) {
503
509
  try {
504
510
  await realpath(resolvedPath);
505
511
  } catch {
506
- throw new Error(`projectPath does not exist: ${resolvedPath}`);
512
+ if (createIfMissing) {
513
+ log.info(`Creating project directory: ${resolvedPath}`);
514
+ await mkdir(resolvedPath, { recursive: true });
515
+ } else {
516
+ throw new Error(`projectPath does not exist: ${resolvedPath}`);
517
+ }
507
518
  }
508
519
 
509
520
  const projectName = resolvedPath.split("/").pop();
@@ -726,7 +737,12 @@ async function runClaudeProcess(sessionId, prompt) {
726
737
  }
727
738
  session.isFirstPrompt = false;
728
739
 
729
- args.push(prompt);
740
+ // IMPORTANT: When --allowedTools is used, it consumes all remaining
741
+ // positional args. So we must pass the prompt via stdin instead.
742
+ const promptViaStdin = !!session.webhookMeta;
743
+ if (!promptViaStdin) {
744
+ args.push(prompt);
745
+ }
730
746
 
731
747
  log.session(sessionId, `Spawning: ${claudeBinary} ${args.join(" ")}`);
732
748
 
@@ -741,9 +757,15 @@ async function runClaudeProcess(sessionId, prompt) {
741
757
 
742
758
  session.process = claudeProcess;
743
759
 
744
- // Close stdin immediately `-p` mode reads the prompt from args, not stdin.
745
- // Leaving stdin open can cause Claude CLI to hang waiting for input.
746
- claudeProcess.stdin.end();
760
+ // When --allowedTools is used (webhook sessions), the prompt is sent via
761
+ // stdin because --allowedTools is variadic and would consume the prompt
762
+ // if passed as a positional arg. Otherwise, close stdin immediately.
763
+ if (promptViaStdin) {
764
+ claudeProcess.stdin.write(prompt);
765
+ claudeProcess.stdin.end();
766
+ } else {
767
+ claudeProcess.stdin.end();
768
+ }
747
769
 
748
770
  log.session(sessionId, `Process started — PID ${claudeProcess.pid}`);
749
771
 
@@ -361,6 +361,21 @@ async function handleWebhookPost(req, res, webhookId, sourceIp) {
361
361
  });
362
362
  }
363
363
 
364
+ // When subscribed to both message and app_mention events, Slack sends
365
+ // separate events for the same user message. Deduplicate by using the
366
+ // Slack message timestamp (ts) which is identical across both events.
367
+ if (event.ts) {
368
+ const tsKey = `slack-ts:${event.channel}:${event.ts}`;
369
+ if (recentDeliveryIds.has(tsKey)) {
370
+ log.info(`Duplicate Slack message ts ignored: ${event.ts}`);
371
+ return sendJson(res, 200, {
372
+ status: "duplicate",
373
+ reason: "same message ts",
374
+ });
375
+ }
376
+ addDeliveryId(tsKey);
377
+ }
378
+
364
379
  // Flatten the Slack event into the payload for template rendering.
365
380
  // This lets templates use {{text}}, {{user}}, {{channel}} directly.
366
381
  payload = { ...payload, ...event };