@chrysb/alphaclaw 0.4.4 → 0.4.5

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.
@@ -1,11 +1,7 @@
1
1
  const os = require("os");
2
2
  const path = require("path");
3
3
  const kBrowseFilePolicies = require("../public/shared/browse-file-policies.json");
4
-
5
- const parsePositiveIntEnv = (value, fallbackValue) => {
6
- const parsed = Number.parseInt(String(value || ""), 10);
7
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackValue;
8
- };
4
+ const { parsePositiveInt } = require("./utils/number");
9
5
 
10
6
  // Portable root directory: --root-dir flag sets ALPHACLAW_ROOT_DIR before require
11
7
  const kRootDir =
@@ -38,29 +34,29 @@ const CODEX_OAUTH_SCOPE = "openid profile email offline_access";
38
34
  const CODEX_JWT_CLAIM_PATH = "https://api.openai.com/auth";
39
35
  const kCodexOauthStateTtlMs = 10 * 60 * 1000;
40
36
 
41
- const kTrustProxyHops = parsePositiveIntEnv(process.env.TRUST_PROXY_HOPS, 1);
42
- const kLoginWindowMs = parsePositiveIntEnv(
37
+ const kTrustProxyHops = parsePositiveInt(process.env.TRUST_PROXY_HOPS, 1);
38
+ const kLoginWindowMs = parsePositiveInt(
43
39
  process.env.LOGIN_RATE_WINDOW_MS,
44
40
  10 * 60 * 1000,
45
41
  );
46
- const kLoginMaxAttempts = parsePositiveIntEnv(
42
+ const kLoginMaxAttempts = parsePositiveInt(
47
43
  process.env.LOGIN_RATE_MAX_ATTEMPTS,
48
44
  5,
49
45
  );
50
- const kLoginBaseLockMs = parsePositiveIntEnv(
46
+ const kLoginBaseLockMs = parsePositiveInt(
51
47
  process.env.LOGIN_RATE_BASE_LOCK_MS,
52
48
  60 * 1000,
53
49
  );
54
- const kLoginMaxLockMs = parsePositiveIntEnv(
50
+ const kLoginMaxLockMs = parsePositiveInt(
55
51
  process.env.LOGIN_RATE_MAX_LOCK_MS,
56
52
  15 * 60 * 1000,
57
53
  );
58
- const kLoginCleanupIntervalMs = parsePositiveIntEnv(
54
+ const kLoginCleanupIntervalMs = parsePositiveInt(
59
55
  process.env.LOGIN_RATE_CLEANUP_INTERVAL_MS,
60
56
  60 * 1000,
61
57
  );
62
58
  const kLoginStateTtlMs = Math.max(
63
- parsePositiveIntEnv(
59
+ parsePositiveInt(
64
60
  process.env.LOGIN_RATE_STATE_TTL_MS,
65
61
  Math.max(kLoginWindowMs, kLoginMaxLockMs) * 3,
66
62
  ),
@@ -116,31 +112,31 @@ const kLatestVersionCacheTtlMs = 10 * 60 * 1000;
116
112
  const kOpenclawRegistryUrl = "https://registry.npmjs.org/openclaw";
117
113
  const kAlphaclawRegistryUrl = "https://registry.npmjs.org/@chrysb%2falphaclaw";
118
114
  const kAppDir = kNpmPackageRoot;
119
- const kMaxPayloadBytes = parsePositiveIntEnv(process.env.WEBHOOK_LOG_MAX_BYTES, 50 * 1024);
120
- const kWebhookPruneDays = parsePositiveIntEnv(process.env.WEBHOOK_LOG_RETENTION_DAYS, 30);
115
+ const kMaxPayloadBytes = parsePositiveInt(process.env.WEBHOOK_LOG_MAX_BYTES, 50 * 1024);
116
+ const kWebhookPruneDays = parsePositiveInt(process.env.WEBHOOK_LOG_RETENTION_DAYS, 30);
121
117
  const kWatchdogCheckIntervalMs =
122
- parsePositiveIntEnv(process.env.WATCHDOG_CHECK_INTERVAL, 120) * 1000;
118
+ parsePositiveInt(process.env.WATCHDOG_CHECK_INTERVAL, 120) * 1000;
123
119
  const kWatchdogDegradedCheckIntervalMs =
124
- parsePositiveIntEnv(process.env.WATCHDOG_DEGRADED_CHECK_INTERVAL, 5) * 1000;
125
- const kWatchdogStartupFailureThreshold = parsePositiveIntEnv(
120
+ parsePositiveInt(process.env.WATCHDOG_DEGRADED_CHECK_INTERVAL, 5) * 1000;
121
+ const kWatchdogStartupFailureThreshold = parsePositiveInt(
126
122
  process.env.WATCHDOG_STARTUP_FAILURE_THRESHOLD,
127
123
  3,
128
124
  );
129
- const kWatchdogMaxRepairAttempts = parsePositiveIntEnv(
125
+ const kWatchdogMaxRepairAttempts = parsePositiveInt(
130
126
  process.env.WATCHDOG_MAX_REPAIR_ATTEMPTS,
131
127
  2,
132
128
  );
133
129
  const kWatchdogCrashLoopWindowMs =
134
- parsePositiveIntEnv(process.env.WATCHDOG_CRASH_LOOP_WINDOW, 300) * 1000;
135
- const kWatchdogCrashLoopThreshold = parsePositiveIntEnv(
130
+ parsePositiveInt(process.env.WATCHDOG_CRASH_LOOP_WINDOW, 300) * 1000;
131
+ const kWatchdogCrashLoopThreshold = parsePositiveInt(
136
132
  process.env.WATCHDOG_CRASH_LOOP_THRESHOLD,
137
133
  3,
138
134
  );
139
- const kWatchdogLogRetentionDays = parsePositiveIntEnv(
135
+ const kWatchdogLogRetentionDays = parsePositiveInt(
140
136
  process.env.WATCHDOG_LOG_RETENTION_DAYS,
141
137
  30,
142
138
  );
143
- const kLogMaxBytes = parsePositiveIntEnv(
139
+ const kLogMaxBytes = parsePositiveInt(
144
140
  process.env.LOG_MAX_BYTES,
145
141
  2 * 1024 * 1024,
146
142
  );
@@ -266,17 +262,17 @@ const GOG_CREDENTIALS_PATH = path.join(GOG_CONFIG_DIR, "credentials.json");
266
262
  const GOG_STATE_PATH = path.join(GOG_CONFIG_DIR, "state.json");
267
263
  const GOG_KEYRING_PASSWORD = process.env.GOG_KEYRING_PASSWORD || "alphaclaw";
268
264
  const kMaxGoogleAccounts = 5;
269
- const kGmailServeBasePort = parsePositiveIntEnv(
265
+ const kGmailServeBasePort = parsePositiveInt(
270
266
  process.env.GMAIL_SERVE_BASE_PORT,
271
267
  18801,
272
268
  );
273
269
  const kGmailWatchRenewalIntervalMs =
274
- parsePositiveIntEnv(process.env.GMAIL_WATCH_RENEWAL_INTERVAL_SECONDS, 6 * 60 * 60) *
270
+ parsePositiveInt(process.env.GMAIL_WATCH_RENEWAL_INTERVAL_SECONDS, 6 * 60 * 60) *
275
271
  1000;
276
272
  const kGmailWatchRenewalThresholdMs =
277
- parsePositiveIntEnv(process.env.GMAIL_WATCH_RENEWAL_THRESHOLD_SECONDS, 24 * 60 * 60) *
273
+ parsePositiveInt(process.env.GMAIL_WATCH_RENEWAL_THRESHOLD_SECONDS, 24 * 60 * 60) *
278
274
  1000;
279
- const kGmailMaxBodyBytes = parsePositiveIntEnv(
275
+ const kGmailMaxBodyBytes = parsePositiveInt(
280
276
  process.env.GMAIL_WATCH_MAX_BODY_BYTES,
281
277
  20000,
282
278
  );
@@ -1,4 +1,14 @@
1
1
  const http = require("http");
2
+ const { parsePositiveInt } = require("./utils/number");
3
+
4
+ const kGmailPushDedupeWindowMs = parsePositiveInt(
5
+ process.env.GMAIL_PUSH_DEDUPE_WINDOW_MS,
6
+ 24 * 60 * 60 * 1000,
7
+ );
8
+ const kGmailPushDedupeMaxEntries = parsePositiveInt(
9
+ process.env.GMAIL_PUSH_DEDUPE_MAX_ENTRIES,
10
+ 50000,
11
+ );
2
12
 
3
13
  const extractBodyBuffer = (body) => {
4
14
  if (Buffer.isBuffer(body)) return body;
@@ -21,6 +31,68 @@ const parsePushEnvelope = (bodyBuffer) => {
21
31
  };
22
32
  };
23
33
 
34
+ const createPushEventDedupeKey = ({ envelope, payload }) => {
35
+ const messageId = String(
36
+ envelope?.message?.messageId || envelope?.message?.message_id || "",
37
+ ).trim();
38
+ if (messageId) return `msg:${messageId}`;
39
+ const email = String(payload?.emailAddress || "")
40
+ .trim()
41
+ .toLowerCase();
42
+ const historyId = String(payload?.historyId || "").trim();
43
+ if (email && historyId) return `hist:${email}:${historyId}`;
44
+ if (historyId) return `hist:${historyId}`;
45
+ return "";
46
+ };
47
+
48
+ const createGmailPushEventDeduper = ({
49
+ ttlMs = kGmailPushDedupeWindowMs,
50
+ maxEntries = kGmailPushDedupeMaxEntries,
51
+ } = {}) => {
52
+ const seenEvents = new Map();
53
+
54
+ const pruneExpiredEntries = (receivedAt) => {
55
+ const cutoff = receivedAt - ttlMs;
56
+ for (const [eventKey, seenAt] of seenEvents.entries()) {
57
+ if (seenAt > cutoff) break;
58
+ seenEvents.delete(eventKey);
59
+ }
60
+ while (seenEvents.size > maxEntries) {
61
+ const oldestKey = seenEvents.keys().next().value;
62
+ if (!oldestKey) break;
63
+ seenEvents.delete(oldestKey);
64
+ }
65
+ };
66
+
67
+ const shouldProcessPushEvent = ({ envelope, payload, receivedAt = Date.now() }) => {
68
+ const timestamp = Number.isFinite(receivedAt) ? receivedAt : Date.now();
69
+ pruneExpiredEntries(timestamp);
70
+ const eventKey = createPushEventDedupeKey({ envelope, payload });
71
+ if (!eventKey) return true;
72
+ return !seenEvents.has(eventKey);
73
+ };
74
+
75
+ shouldProcessPushEvent.markProcessed = ({
76
+ envelope,
77
+ payload,
78
+ receivedAt = Date.now(),
79
+ }) => {
80
+ const timestamp = Number.isFinite(receivedAt) ? receivedAt : Date.now();
81
+ pruneExpiredEntries(timestamp);
82
+ const eventKey = createPushEventDedupeKey({ envelope, payload });
83
+ if (!eventKey) return true;
84
+ seenEvents.set(eventKey, timestamp);
85
+ return true;
86
+ };
87
+
88
+ return shouldProcessPushEvent;
89
+ };
90
+
91
+ const isSuccessfulProxyStatus = (statusCode) => {
92
+ const numericStatus = Number.parseInt(String(statusCode || 0), 10);
93
+ return numericStatus >= 200 && numericStatus < 300;
94
+ };
95
+
24
96
  const proxyPushToServe = async ({
25
97
  port,
26
98
  bodyBuffer,
@@ -58,6 +130,8 @@ const createGmailPushHandler = ({
58
130
  resolvePushToken,
59
131
  resolveTargetByEmail,
60
132
  markPushReceived,
133
+ shouldProcessPushEvent = createGmailPushEventDeduper(),
134
+ proxyPushToServeImpl = proxyPushToServe,
61
135
  }) =>
62
136
  async (req, res) => {
63
137
  try {
@@ -68,11 +142,24 @@ const createGmailPushHandler = ({
68
142
  }
69
143
 
70
144
  const bodyBuffer = extractBodyBuffer(req.body);
71
- const { payload } = parsePushEnvelope(bodyBuffer);
145
+ const { envelope, payload } = parsePushEnvelope(bodyBuffer);
72
146
  const email = String(payload?.emailAddress || "").trim().toLowerCase();
73
147
  if (!email) {
74
148
  return res.status(200).json({ ok: true, ignored: true, reason: "missing_email" });
75
149
  }
150
+ if (
151
+ !shouldProcessPushEvent({
152
+ envelope,
153
+ payload,
154
+ receivedAt: Date.now(),
155
+ })
156
+ ) {
157
+ return res.status(200).json({
158
+ ok: true,
159
+ ignored: true,
160
+ reason: "duplicate_event",
161
+ });
162
+ }
76
163
 
77
164
  const target = resolveTargetByEmail?.(email);
78
165
  if (!target?.port) {
@@ -80,15 +167,22 @@ const createGmailPushHandler = ({
80
167
  }
81
168
 
82
169
  try {
83
- const proxied = await proxyPushToServe({
170
+ const proxied = await proxyPushToServeImpl({
84
171
  port: target.port,
85
172
  bodyBuffer,
86
173
  headers: req.headers || {},
87
174
  });
88
- await markPushReceived?.({
89
- accountId: target.accountId,
90
- at: Date.now(),
91
- });
175
+ if (isSuccessfulProxyStatus(proxied.statusCode)) {
176
+ shouldProcessPushEvent.markProcessed?.({
177
+ envelope,
178
+ payload,
179
+ receivedAt: Date.now(),
180
+ });
181
+ await markPushReceived?.({
182
+ accountId: target.accountId,
183
+ at: Date.now(),
184
+ });
185
+ }
92
186
  return res
93
187
  .status(proxied.statusCode)
94
188
  .send(proxied.body || "");
@@ -106,4 +200,6 @@ const createGmailPushHandler = ({
106
200
 
107
201
  module.exports = {
108
202
  createGmailPushHandler,
203
+ createGmailPushEventDeduper,
204
+ createPushEventDedupeKey,
109
205
  };
@@ -14,29 +14,14 @@ const {
14
14
  allocateServePort,
15
15
  } = require("./google-state");
16
16
  const { createGmailServeManager } = require("./gmail-serve");
17
+ const { parseJsonObjectFromNoisyOutput, parseJsonSafe } = require("./utils/json");
17
18
  const { createWebhook } = require("./webhooks");
18
-
19
- const quoteShellArg = (value) =>
20
- `"${String(value || "").replace(/(["\\$`])/g, "\\$1")}"`;
21
-
22
- const parseJsonMaybe = (raw) => {
23
- const text = String(raw || "").trim();
24
- if (!text) return null;
25
- try {
26
- return JSON.parse(text);
27
- } catch {}
28
- const firstBrace = text.indexOf("{");
29
- const lastBrace = text.lastIndexOf("}");
30
- if (firstBrace >= 0 && lastBrace > firstBrace) {
31
- try {
32
- return JSON.parse(text.slice(firstBrace, lastBrace + 1));
33
- } catch {}
34
- }
35
- return null;
36
- };
19
+ const { quoteShellArg } = require("./utils/shell");
37
20
 
38
21
  const parseExpirationFromOutput = (raw) => {
39
- const parsed = parseJsonMaybe(raw);
22
+ const parsed =
23
+ parseJsonSafe(raw, null, { trim: true }) ||
24
+ parseJsonObjectFromNoisyOutput(raw);
40
25
  if (parsed?.expiration) {
41
26
  const numeric = Number.parseInt(String(parsed.expiration), 10);
42
27
  if (Number.isFinite(numeric) && numeric > 0) return numeric;
@@ -5,6 +5,9 @@ const {
5
5
  kOnboardingModelProviders,
6
6
  gogClientCredentialsPath,
7
7
  } = require("./constants");
8
+ const { isTruthyFlag } = require("./utils/boolean");
9
+ const { parseJsonObjectFromNoisyOutput } = require("./utils/json");
10
+ const { normalizeIp } = require("./utils/network");
8
11
 
9
12
  const normalizeOpenclawVersion = (rawVersion) => {
10
13
  if (!rawVersion) return null;
@@ -32,19 +35,7 @@ const compareVersionParts = (a, b) => {
32
35
  return 0;
33
36
  };
34
37
 
35
- const parseJsonFromNoisyOutput = (raw) => {
36
- const text = String(raw || "");
37
- const firstBrace = text.indexOf("{");
38
- const lastBrace = text.lastIndexOf("}");
39
- if (firstBrace === -1 || lastBrace === -1 || lastBrace <= firstBrace) {
40
- return null;
41
- }
42
- try {
43
- return JSON.parse(text.slice(firstBrace, lastBrace + 1));
44
- } catch {
45
- return null;
46
- }
47
- };
38
+ const parseJsonFromNoisyOutput = (raw) => parseJsonObjectFromNoisyOutput(raw);
48
39
 
49
40
  const parseJwtPayload = (token) => {
50
41
  try {
@@ -63,14 +54,7 @@ const getCodexAccountId = (accessToken) => {
63
54
  return typeof accountId === "string" && accountId ? accountId : null;
64
55
  };
65
56
 
66
- const normalizeIp = (ip) => String(ip || "").replace(/^::ffff:/, "");
67
-
68
- const isTruthyEnvFlag = (value) =>
69
- ["1", "true", "yes", "on"].includes(
70
- String(value || "")
71
- .trim()
72
- .toLowerCase(),
73
- );
57
+ const isTruthyEnvFlag = (value) => isTruthyFlag(value);
74
58
  const isDebugEnabled = () =>
75
59
  isTruthyEnvFlag(process.env.ALPHACLAW_DEBUG) ||
76
60
  isTruthyEnvFlag(process.env.DEBUG);
@@ -12,16 +12,8 @@ const {
12
12
  } = require("../google-state");
13
13
  const { syncBootstrapPromptFiles } = require("../onboarding/workspace");
14
14
  const { installGogCliSkill } = require("../gog-skill");
15
-
16
- const quoteShellArg = (value) => `"${String(value || "").replace(/(["\\$`])/g, "\\$1")}"`;
17
-
18
- const parseJsonSafe = (raw, fallbackValue) => {
19
- try {
20
- return JSON.parse(String(raw || ""));
21
- } catch {
22
- return fallbackValue;
23
- }
24
- };
15
+ const { parseJsonSafe } = require("../utils/json");
16
+ const { quoteShellArg } = require("../utils/shell");
25
17
 
26
18
  const uniqueServiceLabels = (scopes) =>
27
19
  Array.from(
@@ -1,19 +1,10 @@
1
1
  const fs = require("fs");
2
2
  const { OPENCLAW_DIR } = require("../constants");
3
3
  const { isDebugEnabled } = require("../helpers");
4
+ const { parseBooleanValue } = require("../utils/boolean");
5
+ const { quoteShellArg } = require("../utils/shell");
4
6
  const topicRegistry = require("../topic-registry");
5
7
  const { syncConfigForTelegram } = require("../telegram-workspace");
6
-
7
- const parseBooleanValue = (value, fallbackValue = false) => {
8
- if (typeof value === "boolean") return value;
9
- if (typeof value === "number") return value !== 0;
10
- if (typeof value === "string") {
11
- const normalized = value.trim().toLowerCase();
12
- if (["true", "1", "yes", "on"].includes(normalized)) return true;
13
- if (["false", "0", "no", "off", ""].includes(normalized)) return false;
14
- }
15
- return fallbackValue;
16
- };
17
8
  const resolveGroupId = (req) => {
18
9
  const body = req.body || {};
19
10
  const rawGroupId = body.groupId ?? body.chatId;
@@ -53,13 +44,11 @@ const normalizeGitSyncMessagePart = (value) =>
53
44
  .replace(/\s+/g, " ")
54
45
  .trim();
55
46
 
56
- const quoteShellArg = (value) => `'${String(value || "").replace(/'/g, `'\"'\"'`)}'`;
57
-
58
47
  const buildTelegramGitSyncCommand = (action, target = "") => {
59
48
  const safeAction = normalizeGitSyncMessagePart(action);
60
49
  const safeTarget = normalizeGitSyncMessagePart(target);
61
50
  const message = `telegram workspace: ${safeAction} ${safeTarget}`.trim();
62
- return `alphaclaw git-sync -m ${quoteShellArg(message)}`;
51
+ return `alphaclaw git-sync -m ${quoteShellArg(message, { strategy: "single" })}`;
63
52
  };
64
53
 
65
54
  const registerTelegramRoutes = ({
@@ -1,13 +1,9 @@
1
1
  const topicRegistry = require("../topic-registry");
2
+ const { parsePositiveInt } = require("../utils/number");
2
3
 
3
4
  const kSummaryCacheTtlMs = 60 * 1000;
4
5
  const kClientTimeZoneHeader = "x-client-timezone";
5
6
 
6
- const parsePositiveInt = (value, fallbackValue) => {
7
- const parsed = Number.parseInt(String(value ?? ""), 10);
8
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackValue;
9
- };
10
-
11
7
  const createSummaryCache = () => new Map();
12
8
  const toTitleLabel = (value) => {
13
9
  const raw = String(value || "").trim();
@@ -5,15 +5,11 @@ const {
5
5
  deleteWebhook,
6
6
  validateWebhookName,
7
7
  } = require("../webhooks");
8
+ const { isTruthyFlag } = require("../utils/boolean");
8
9
 
9
10
  const isFiniteInteger = (value) =>
10
11
  Number.isFinite(value) && Number.isInteger(value);
11
- const parseBooleanFlag = (value) => {
12
- const normalized = String(value == null ? "" : value)
13
- .trim()
14
- .toLowerCase();
15
- return ["1", "true", "yes", "on"].includes(normalized);
16
- };
12
+ const parseBooleanFlag = (value) => isTruthyFlag(value);
17
13
 
18
14
  const buildHealth = ({ totalCount, errorCount }) => {
19
15
  if (!totalCount || totalCount <= 0) return "green";
@@ -0,0 +1,22 @@
1
+ const isTruthyFlag = (value) =>
2
+ ["1", "true", "yes", "on"].includes(
3
+ String(value ?? "")
4
+ .trim()
5
+ .toLowerCase(),
6
+ );
7
+
8
+ const parseBooleanValue = (value, fallbackValue = false) => {
9
+ if (typeof value === "boolean") return value;
10
+ if (typeof value === "number") return value !== 0;
11
+ if (typeof value === "string") {
12
+ const normalized = value.trim().toLowerCase();
13
+ if (["true", "1", "yes", "on"].includes(normalized)) return true;
14
+ if (["false", "0", "no", "off", ""].includes(normalized)) return false;
15
+ }
16
+ return fallbackValue;
17
+ };
18
+
19
+ module.exports = {
20
+ isTruthyFlag,
21
+ parseBooleanValue,
22
+ };
@@ -0,0 +1,31 @@
1
+ const parseJsonSafe = (rawValue, fallbackValue = null, options = {}) => {
2
+ const shouldTrim = options?.trim === true;
3
+ const text = shouldTrim
4
+ ? String(rawValue ?? "").trim()
5
+ : String(rawValue ?? "");
6
+ if (!text) return fallbackValue;
7
+ try {
8
+ return JSON.parse(text);
9
+ } catch {
10
+ return fallbackValue;
11
+ }
12
+ };
13
+
14
+ const parseJsonObjectFromNoisyOutput = (rawValue) => {
15
+ const text = String(rawValue ?? "");
16
+ const firstBrace = text.indexOf("{");
17
+ const lastBrace = text.lastIndexOf("}");
18
+ if (firstBrace === -1 || lastBrace === -1 || lastBrace <= firstBrace) {
19
+ return null;
20
+ }
21
+ try {
22
+ return JSON.parse(text.slice(firstBrace, lastBrace + 1));
23
+ } catch {
24
+ return null;
25
+ }
26
+ };
27
+
28
+ module.exports = {
29
+ parseJsonSafe,
30
+ parseJsonObjectFromNoisyOutput,
31
+ };
@@ -0,0 +1,5 @@
1
+ const normalizeIp = (ip) => String(ip || "").replace(/^::ffff:/, "");
2
+
3
+ module.exports = {
4
+ normalizeIp,
5
+ };
@@ -0,0 +1,8 @@
1
+ const parsePositiveInt = (value, fallbackValue) => {
2
+ const parsed = Number.parseInt(String(value ?? ""), 10);
3
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackValue;
4
+ };
5
+
6
+ module.exports = {
7
+ parsePositiveInt,
8
+ };
@@ -0,0 +1,16 @@
1
+ const quoteShellArg = (value, options = {}) => {
2
+ const strategy = String(options?.strategy || "double").trim().toLowerCase();
3
+ const normalizedValue = String(value || "");
4
+
5
+ if (strategy === "single") {
6
+ return `'${normalizedValue.replace(/'/g, `'\"'\"'`)}'`;
7
+ }
8
+ if (strategy === "double") {
9
+ return `"${normalizedValue.replace(/(["\\$`])/g, "\\$1")}"`;
10
+ }
11
+ throw new Error(`Unsupported shell quote strategy: ${strategy}`);
12
+ };
13
+
14
+ module.exports = {
15
+ quoteShellArg,
16
+ };
@@ -1,13 +1,12 @@
1
1
  const http = require("http");
2
2
  const https = require("https");
3
3
  const { URL } = require("url");
4
+ const { normalizeIp } = require("./utils/network");
4
5
 
5
6
  const kRedactedHeaderKeys = new Set(["authorization", "cookie", "x-webhook-token"]);
6
7
  const kGmailDedupeTtlMs = 24 * 60 * 60 * 1000;
7
8
  const kGmailDedupeCleanupIntervalMs = 60 * 1000;
8
9
 
9
- const normalizeIp = (ip) => String(ip || "").replace(/^::ffff:/, "");
10
-
11
10
  const sanitizeHeaders = (headers) => {
12
11
  const sanitized = {};
13
12
  for (const [key, value] of Object.entries(headers || {})) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },