replicas-engine 0.1.703 → 0.1.704
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/src/chunk-2RB7SIP3.js +10 -0
- package/dist/src/{chunk-U3XQBTVU.js → chunk-EC4MFY4M.js} +2 -2
- package/dist/src/chunk-GDCMH7YA.js +23 -0
- package/dist/src/{chunk-LPJ6WYD4.js → chunk-WUJ4IALO.js} +40 -0
- package/dist/src/{chunk-RVDZXLBY.js → chunk-ZCBMGGAC.js} +4 -7
- package/dist/src/command-protection-hook.js +5 -3
- package/dist/src/deepseek-command-protection-plugin.js +11 -2
- package/dist/src/headless-agent.js +2 -2
- package/dist/src/index.js +152 -58
- package/dist/src/post-tool-pr-hook.js +26 -0
- package/package.json +2 -1
- package/scripts/opencode-command-protection-plugin.ts +19 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
HOOK_EXEC_MAX_BUFFER_BYTES,
|
|
4
4
|
isVersionBelow
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-WUJ4IALO.js";
|
|
6
6
|
|
|
7
7
|
// src/utils/presigned-upload.ts
|
|
8
8
|
import { createReadStream } from "fs";
|
|
@@ -267,7 +267,7 @@ var DEFAULT_CODEX_ARGS = [
|
|
|
267
267
|
var MIN_CODEX_CLI_VERSION = "0.144.6";
|
|
268
268
|
var CODEX_UPGRADE_TIMEOUT_MS = 12e4;
|
|
269
269
|
var codexCliVersionEnsured = null;
|
|
270
|
-
var ENGINE_PACKAGE_VERSION = "0.1.
|
|
270
|
+
var ENGINE_PACKAGE_VERSION = "0.1.704";
|
|
271
271
|
var INITIALIZE_METHOD = "initialize";
|
|
272
272
|
var INITIALIZED_NOTIFICATION = "initialized";
|
|
273
273
|
var ACCOUNT_LOGIN_START_METHOD = "account/login/start";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
mayCreatePullRequest
|
|
4
|
+
} from "./chunk-WUJ4IALO.js";
|
|
5
|
+
|
|
6
|
+
// src/services/post-tool-pr-notifier.ts
|
|
7
|
+
async function notifyPostToolUse(toolName, toolInput) {
|
|
8
|
+
if (!mayCreatePullRequest(toolName, toolInput)) return false;
|
|
9
|
+
const port = process.env.REPLICAS_ENGINE_PORT;
|
|
10
|
+
const secret = process.env.REPLICAS_ENGINE_SECRET;
|
|
11
|
+
if (!port || !secret) throw new Error("Replicas engine connection is unavailable.");
|
|
12
|
+
const response = await fetch(`http://127.0.0.1:${port}/pull-requests/check`, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: { "X-Replicas-Engine-Secret": secret },
|
|
15
|
+
signal: AbortSignal.timeout(3e4)
|
|
16
|
+
});
|
|
17
|
+
if (!response.ok) throw new Error(`Replicas pull request check failed (${response.status}).`);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
notifyPostToolUse
|
|
23
|
+
};
|
|
@@ -1128,6 +1128,13 @@ async function raceWithTimeout(promise, ms) {
|
|
|
1128
1128
|
});
|
|
1129
1129
|
}
|
|
1130
1130
|
}
|
|
1131
|
+
async function withTimeout(promise, ms, message) {
|
|
1132
|
+
const result = await raceWithTimeout(promise, ms);
|
|
1133
|
+
if (result === TIMEOUT) {
|
|
1134
|
+
throw new Error(message);
|
|
1135
|
+
}
|
|
1136
|
+
return result;
|
|
1137
|
+
}
|
|
1131
1138
|
|
|
1132
1139
|
// ../shared/src/agent-quota.ts
|
|
1133
1140
|
var OUT_OF_CREDITS_PATTERNS = [
|
|
@@ -3659,6 +3666,37 @@ function findGitCommitSignals(request) {
|
|
|
3659
3666
|
const command = extractCommandProtectionCommandText(request, { stringifyFallback: true }) ?? "";
|
|
3660
3667
|
return findGitCommitCommands(command).length > 0 ? ["git-commit"] : [];
|
|
3661
3668
|
}
|
|
3669
|
+
function mayCreatePullRequest(toolName, toolInput) {
|
|
3670
|
+
const input = normalizeCommandProtectionText(stringifyCommandProtectionInput(toolInput));
|
|
3671
|
+
const combined = `${toolName.toLowerCase()} ${input}`;
|
|
3672
|
+
return [
|
|
3673
|
+
/\bgit\s+push\b/,
|
|
3674
|
+
/\bgit\s+(?:branch|checkout|switch|pull|merge|rebase|reset)\b/,
|
|
3675
|
+
/\bgit\s+worktree\s+add\b/,
|
|
3676
|
+
/\bgh\b.{0,120}\bpr\s+(?:checkout|create|new|ready|reopen)\b/,
|
|
3677
|
+
/\bgh\s+api\b[\s\S]*(?:\/repos\/[^/\s]+\/[^/\s]+\/pulls\b|\bcreatepullrequest\b)/,
|
|
3678
|
+
/\b(?:glab|gitlab)\s+mr\s+(?:checkout|create)\b/,
|
|
3679
|
+
/\bhub\s+pull-request\b/,
|
|
3680
|
+
/\b(?:tea\s+pulls|bb\s+pr|az\s+repos\s+pr)\s+create\b/,
|
|
3681
|
+
/\bcreatepullrequest\b/,
|
|
3682
|
+
/\b(?:create|open|submit)[_-]?(?:pr|pull[_-]?request|mr|merge[_-]?request)\b/,
|
|
3683
|
+
/\b(?:pr|pull[_-]?request|mr|merge[_-]?request)[_-]?(?:create|open|submit)\b/,
|
|
3684
|
+
/\b(?:checkout|create|switch)[_-]?branch\b/,
|
|
3685
|
+
/\bbranch[_-]?(?:checkout|create|switch)\b/,
|
|
3686
|
+
/\b(?:switched to|checked out|created and switched to)(?: a new)? branch\b/,
|
|
3687
|
+
/github\.com\/[^/\s]+\/[^/\s]+\/pull\/\d+\b/,
|
|
3688
|
+
/\/merge_requests\/\d+\b/,
|
|
3689
|
+
/\b(?:create|open|submit)(?:pull|merge)request\b/,
|
|
3690
|
+
/\b(?:pull|merge)request(?:create|open|submit)\b/,
|
|
3691
|
+
/\bmergerequestcreate\b/,
|
|
3692
|
+
/api\.github\.com\/repos\/[^/\s]+\/[^/\s]+\/pulls\b/,
|
|
3693
|
+
/\/projects\/[^/\s]+\/merge_requests\b/,
|
|
3694
|
+
/\/pullrequests\b/,
|
|
3695
|
+
/\/_apis\/git\/repositories\/[^/\s]+\/pullrequests\b/,
|
|
3696
|
+
/\/pulls\b[\s\S]*\b(?:post|create)\b/,
|
|
3697
|
+
/\/merge_requests\b[\s\S]*\b(?:post|create)\b/
|
|
3698
|
+
].some((pattern) => pattern.test(combined));
|
|
3699
|
+
}
|
|
3662
3700
|
|
|
3663
3701
|
// ../shared/src/routes/plugin-catalog.ts
|
|
3664
3702
|
var EXPLICIT_PLUGIN_CATALOG = [
|
|
@@ -6579,6 +6617,7 @@ export {
|
|
|
6579
6617
|
isNonEmptyString,
|
|
6580
6618
|
TIMEOUT,
|
|
6581
6619
|
raceWithTimeout,
|
|
6620
|
+
withTimeout,
|
|
6582
6621
|
createSuccessResult,
|
|
6583
6622
|
createErrorResult,
|
|
6584
6623
|
AGENT,
|
|
@@ -6725,6 +6764,7 @@ export {
|
|
|
6725
6764
|
extractCommandProtectionCommandText,
|
|
6726
6765
|
findPrMergeSignals,
|
|
6727
6766
|
findGitCommitSignals,
|
|
6767
|
+
mayCreatePullRequest,
|
|
6728
6768
|
coerceBackgroundTaskPayload,
|
|
6729
6769
|
isTerminalBackgroundTaskStatus,
|
|
6730
6770
|
areUserMessagesWithinMatchWindow,
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
isRecord as isRecord2
|
|
4
|
+
} from "./chunk-2RB7SIP3.js";
|
|
2
5
|
import {
|
|
3
6
|
REPLICAS_RUNTIME_ENV_ALIASES,
|
|
4
7
|
agentCredentialSnapshotSchema,
|
|
@@ -9,12 +12,7 @@ import {
|
|
|
9
12
|
isValidAgentProvider,
|
|
10
13
|
parsePosixEnvFile,
|
|
11
14
|
readReplicasRuntimeEnv
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
|
|
14
|
-
// src/utils/type-guards.ts
|
|
15
|
-
function isRecord2(value) {
|
|
16
|
-
return typeof value === "object" && value !== null;
|
|
17
|
-
}
|
|
15
|
+
} from "./chunk-WUJ4IALO.js";
|
|
18
16
|
|
|
19
17
|
// src/engine-env.ts
|
|
20
18
|
import { readFileSync as readFileSync2 } from "fs";
|
|
@@ -275,7 +273,6 @@ function reportCommandProtectionBlock(options) {
|
|
|
275
273
|
}
|
|
276
274
|
|
|
277
275
|
export {
|
|
278
|
-
isRecord2 as isRecord,
|
|
279
276
|
IS_WARMING_MODE,
|
|
280
277
|
ENGINE_ENV,
|
|
281
278
|
setAgentCredentialSnapshot,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
evaluateCommandProtection
|
|
3
|
+
evaluateCommandProtection
|
|
4
|
+
} from "./chunk-ZCBMGGAC.js";
|
|
5
|
+
import {
|
|
4
6
|
isRecord
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
7
|
+
} from "./chunk-2RB7SIP3.js";
|
|
8
|
+
import "./chunk-WUJ4IALO.js";
|
|
7
9
|
|
|
8
10
|
// src/command-protection-hook.ts
|
|
9
11
|
var provider = process.argv[2];
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
evaluateCommandProtection
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import
|
|
4
|
+
} from "./chunk-ZCBMGGAC.js";
|
|
5
|
+
import {
|
|
6
|
+
notifyPostToolUse
|
|
7
|
+
} from "./chunk-GDCMH7YA.js";
|
|
8
|
+
import "./chunk-2RB7SIP3.js";
|
|
9
|
+
import "./chunk-WUJ4IALO.js";
|
|
6
10
|
|
|
7
11
|
// src/deepseek-command-protection-plugin.ts
|
|
8
12
|
function replicasCommandProtection(context) {
|
|
@@ -17,6 +21,11 @@ function replicasCommandProtection(context) {
|
|
|
17
21
|
});
|
|
18
22
|
return result.allowed ? next() : { kind: "deny", reason: result.reason ?? "Blocked by Replicas command protection." };
|
|
19
23
|
});
|
|
24
|
+
context.on("tools/post-execute", async (execution, _result, next) => {
|
|
25
|
+
await notifyPostToolUse(execution.name, execution.arguments).catch(() => {
|
|
26
|
+
});
|
|
27
|
+
return next();
|
|
28
|
+
});
|
|
20
29
|
}
|
|
21
30
|
export {
|
|
22
31
|
replicasCommandProtection as default
|
|
@@ -4,12 +4,12 @@ import {
|
|
|
4
4
|
buildCodexAgentEnv,
|
|
5
5
|
putPresignedFile,
|
|
6
6
|
recoverCompletedTurn
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-EC4MFY4M.js";
|
|
8
8
|
import {
|
|
9
9
|
AGENT,
|
|
10
10
|
getMemoryOutputSafetyViolation,
|
|
11
11
|
headlessAgentRequestSchema
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-WUJ4IALO.js";
|
|
13
13
|
|
|
14
14
|
// src/headless-agent.ts
|
|
15
15
|
import { createHash } from "crypto";
|
package/dist/src/index.js
CHANGED
|
@@ -4,12 +4,11 @@ import {
|
|
|
4
4
|
IS_WARMING_MODE,
|
|
5
5
|
evaluateCommandProtection,
|
|
6
6
|
extractToolCommand,
|
|
7
|
-
isRecord as isRecord2,
|
|
8
7
|
monolithRequest,
|
|
9
8
|
monolithService,
|
|
10
9
|
reportCommandProtectionBlock,
|
|
11
10
|
setAgentCredentialSnapshot
|
|
12
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-ZCBMGGAC.js";
|
|
13
12
|
import {
|
|
14
13
|
ACCOUNT_RATE_LIMITS_UPDATED_METHOD,
|
|
15
14
|
AGENT_MESSAGE_DELTA_METHOD,
|
|
@@ -36,7 +35,10 @@ import {
|
|
|
36
35
|
execFileAsync,
|
|
37
36
|
putPresignedFile,
|
|
38
37
|
recoverCompletedTurn
|
|
39
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-EC4MFY4M.js";
|
|
39
|
+
import {
|
|
40
|
+
isRecord as isRecord2
|
|
41
|
+
} from "./chunk-2RB7SIP3.js";
|
|
40
42
|
import {
|
|
41
43
|
ACCEPTED_USER_MESSAGE_SOURCE,
|
|
42
44
|
AGENT,
|
|
@@ -182,6 +184,7 @@ import {
|
|
|
182
184
|
isTransientErrorText,
|
|
183
185
|
isValidAgentProvider,
|
|
184
186
|
isValidEngineLogSessionId,
|
|
187
|
+
mayCreatePullRequest,
|
|
185
188
|
mergeSlashCommands,
|
|
186
189
|
normalizeAutoCompactThreshold,
|
|
187
190
|
normalizeClaudeModel,
|
|
@@ -207,8 +210,9 @@ import {
|
|
|
207
210
|
sendChatMessageRequestSchema,
|
|
208
211
|
serializeCanvasContentResponse,
|
|
209
212
|
shellQuotePosix,
|
|
210
|
-
stripAgentDiagnosticErrors
|
|
211
|
-
|
|
213
|
+
stripAgentDiagnosticErrors,
|
|
214
|
+
withTimeout
|
|
215
|
+
} from "./chunk-WUJ4IALO.js";
|
|
212
216
|
|
|
213
217
|
// src/index.ts
|
|
214
218
|
import { serve } from "@hono/node-server";
|
|
@@ -3949,6 +3953,92 @@ var LinearEventForwarder = class {
|
|
|
3949
3953
|
}
|
|
3950
3954
|
};
|
|
3951
3955
|
|
|
3956
|
+
// src/services/upload-repo-state.ts
|
|
3957
|
+
var uploadQueue = Promise.resolve();
|
|
3958
|
+
async function sendRepoState(repos) {
|
|
3959
|
+
const form = new FormData();
|
|
3960
|
+
form.append(
|
|
3961
|
+
"file",
|
|
3962
|
+
new Blob([JSON.stringify({ repos })], { type: "application/json" }),
|
|
3963
|
+
"repo-state.json"
|
|
3964
|
+
);
|
|
3965
|
+
const response = await monolithRequest("/v1/engine/repo-state", { body: form });
|
|
3966
|
+
if (!response.ok) {
|
|
3967
|
+
const errorText = await response.text();
|
|
3968
|
+
throw new Error(`upload failed: ${response.status} ${errorText}`);
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
function uploadRepoState(repos) {
|
|
3972
|
+
const upload = uploadQueue.then(() => sendRepoState(repos));
|
|
3973
|
+
uploadQueue = upload.catch(() => {
|
|
3974
|
+
});
|
|
3975
|
+
return upload;
|
|
3976
|
+
}
|
|
3977
|
+
async function flushRepoState() {
|
|
3978
|
+
try {
|
|
3979
|
+
const { repos, complete } = await gitService.listReposWithCompleteness({ includeDiffs: true });
|
|
3980
|
+
if (!complete) return { flushed: 0, skipped: 1, failed: 0 };
|
|
3981
|
+
await uploadRepoState(repos);
|
|
3982
|
+
return { flushed: 1, skipped: 0, failed: 0 };
|
|
3983
|
+
} catch (err) {
|
|
3984
|
+
console.error("[RepoStateUploader] upload failed:", err);
|
|
3985
|
+
return { flushed: 0, skipped: 0, failed: 1 };
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3988
|
+
|
|
3989
|
+
// src/services/refresh-repo-state.ts
|
|
3990
|
+
async function refreshRepoStatuses(observedBranchesByRepo, includeDiffs = false) {
|
|
3991
|
+
await Promise.all([
|
|
3992
|
+
githubTokenManager.refreshOnce().catch(() => {
|
|
3993
|
+
}),
|
|
3994
|
+
gitlabTokenManager.refreshOnce().catch(() => {
|
|
3995
|
+
})
|
|
3996
|
+
]);
|
|
3997
|
+
const { repos, complete } = await gitService.refreshReposWithCompleteness(
|
|
3998
|
+
observedBranchesByRepo,
|
|
3999
|
+
{ includeDiffs }
|
|
4000
|
+
);
|
|
4001
|
+
if (complete) {
|
|
4002
|
+
uploadRepoState(repos).catch((error) => {
|
|
4003
|
+
console.error("[RefreshRepoState] Failed to upload repository state:", error);
|
|
4004
|
+
});
|
|
4005
|
+
}
|
|
4006
|
+
return repos;
|
|
4007
|
+
}
|
|
4008
|
+
|
|
4009
|
+
// src/services/pull-request-check-service.ts
|
|
4010
|
+
var CHECK_TIMEOUT_MS = 3e4;
|
|
4011
|
+
var PullRequestCheckService = class {
|
|
4012
|
+
running = null;
|
|
4013
|
+
trailing = null;
|
|
4014
|
+
// Plain single-flight would hand a caller a result gathered before its PR
|
|
4015
|
+
// existed, so callers arriving mid-run instead share one trailing run that
|
|
4016
|
+
// starts after the current one settles.
|
|
4017
|
+
check() {
|
|
4018
|
+
const running = this.running;
|
|
4019
|
+
if (!running) return this.start();
|
|
4020
|
+
this.trailing ??= running.catch(() => {
|
|
4021
|
+
}).then(() => {
|
|
4022
|
+
this.trailing = null;
|
|
4023
|
+
return this.start();
|
|
4024
|
+
});
|
|
4025
|
+
return this.trailing;
|
|
4026
|
+
}
|
|
4027
|
+
start() {
|
|
4028
|
+
const run = withTimeout(this.refresh(), CHECK_TIMEOUT_MS, "Pull request check timed out").finally(() => {
|
|
4029
|
+
if (this.running === run) this.running = null;
|
|
4030
|
+
});
|
|
4031
|
+
this.running = run;
|
|
4032
|
+
return run;
|
|
4033
|
+
}
|
|
4034
|
+
async refresh() {
|
|
4035
|
+
const repoStatuses = await refreshRepoStatuses();
|
|
4036
|
+
await monolithService.sendEvent({ type: "pull_request_check", payload: { repoStatuses } });
|
|
4037
|
+
return repoStatuses;
|
|
4038
|
+
}
|
|
4039
|
+
};
|
|
4040
|
+
var pullRequestCheckService = new PullRequestCheckService();
|
|
4041
|
+
|
|
3952
4042
|
// src/services/skill-registry-service.ts
|
|
3953
4043
|
import { readFile as readFile8, readdir as readdir3, stat as stat2 } from "fs/promises";
|
|
3954
4044
|
import { dirname as dirname3, isAbsolute, join as join14, relative, resolve } from "path";
|
|
@@ -5152,6 +5242,17 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
|
|
|
5152
5242
|
matcher: "*",
|
|
5153
5243
|
hooks: [this.buildCommandProtectionHook()],
|
|
5154
5244
|
timeout: 30
|
|
5245
|
+
}],
|
|
5246
|
+
PostToolUse: [{
|
|
5247
|
+
matcher: "*",
|
|
5248
|
+
hooks: [async (input) => {
|
|
5249
|
+
if (input.hook_event_name === "PostToolUse" && mayCreatePullRequest(input.tool_name, input.tool_input)) {
|
|
5250
|
+
pullRequestCheckService.check().catch(() => {
|
|
5251
|
+
});
|
|
5252
|
+
}
|
|
5253
|
+
return {};
|
|
5254
|
+
}],
|
|
5255
|
+
timeout: 30
|
|
5155
5256
|
}]
|
|
5156
5257
|
}
|
|
5157
5258
|
}
|
|
@@ -7101,6 +7202,11 @@ var CodexAspManager = class extends CodingAgentManager {
|
|
|
7101
7202
|
);
|
|
7102
7203
|
this.emitTranscriptUpdated(notification.params.threadId, { immediate: true });
|
|
7103
7204
|
linearForwarder.sendEvent(convertCodexAspNotification(notification, linearSessionId ?? ""));
|
|
7205
|
+
const completedItem = notification.params.item;
|
|
7206
|
+
if (completedItem.type === "commandExecution" && completedItem.exitCode === 0 && mayCreatePullRequest("commandExecution", { command: completedItem.command }) || completedItem.type === "mcpToolCall" && completedItem.status === "completed" && mayCreatePullRequest(completedItem.tool, completedItem.arguments) || completedItem.type === "dynamicToolCall" && completedItem.success !== false && mayCreatePullRequest(completedItem.tool, completedItem.arguments)) {
|
|
7207
|
+
pullRequestCheckService.check().catch(() => {
|
|
7208
|
+
});
|
|
7209
|
+
}
|
|
7104
7210
|
},
|
|
7105
7211
|
[AGENT_MESSAGE_DELTA_METHOD]: (notification) => {
|
|
7106
7212
|
if (!matchesTurn(notification.params.threadId, notification.params.turnId)) return;
|
|
@@ -7823,15 +7929,28 @@ var hookPath = [
|
|
|
7823
7929
|
join18(moduleDirectory, "command-protection-hook.js"),
|
|
7824
7930
|
join18(moduleDirectory, "..", "command-protection-hook.ts")
|
|
7825
7931
|
].find(existsSync6);
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7932
|
+
var postToolHookPath = [
|
|
7933
|
+
join18(moduleDirectory, "post-tool-pr-hook.js"),
|
|
7934
|
+
join18(moduleDirectory, "..", "post-tool-pr-hook.ts")
|
|
7935
|
+
].find(existsSync6);
|
|
7936
|
+
function shellCommand(path6, provider) {
|
|
7937
|
+
if (!path6) throw new Error("Replicas hook executable is missing.");
|
|
7938
|
+
const args = [
|
|
7939
|
+
process.execPath,
|
|
7940
|
+
...path6.endsWith(".ts") && basename(process.execPath) !== "bun" ? ["--import", "tsx"] : [],
|
|
7941
|
+
path6,
|
|
7942
|
+
...provider ? [provider] : []
|
|
7943
|
+
];
|
|
7829
7944
|
return args.map((value) => `'${value.replaceAll("'", "'\\''")}'`).join(" ");
|
|
7830
7945
|
}
|
|
7831
7946
|
function getCommandProtectionHookPath() {
|
|
7832
7947
|
if (!hookPath) throw new Error("Replicas command protection hook executable is missing.");
|
|
7833
7948
|
return hookPath;
|
|
7834
7949
|
}
|
|
7950
|
+
function getPostToolPrHookPath() {
|
|
7951
|
+
if (!postToolHookPath) throw new Error("Replicas post-tool PR hook executable is missing.");
|
|
7952
|
+
return postToolHookPath;
|
|
7953
|
+
}
|
|
7835
7954
|
async function ensureCursorCommandProtectionHook() {
|
|
7836
7955
|
const path6 = join18(ENGINE_ENV.HOME_DIR, ".cursor", "hooks.json");
|
|
7837
7956
|
let config = {};
|
|
@@ -7846,7 +7965,12 @@ async function ensureCursorCommandProtectionHook() {
|
|
|
7846
7965
|
const existing = Array.isArray(hooks.preToolUse) ? hooks.preToolUse : [];
|
|
7847
7966
|
hooks.preToolUse = [
|
|
7848
7967
|
...existing.filter((entry) => !isRecord2(entry) || typeof entry.command !== "string" || !entry.command.includes("command-protection-hook")),
|
|
7849
|
-
{ command: shellCommand("cursor") }
|
|
7968
|
+
{ command: shellCommand(hookPath, "cursor") }
|
|
7969
|
+
];
|
|
7970
|
+
const existingPost = Array.isArray(hooks.postToolUse) ? hooks.postToolUse : [];
|
|
7971
|
+
hooks.postToolUse = [
|
|
7972
|
+
...existingPost.filter((entry) => !isRecord2(entry) || typeof entry.command !== "string" || !entry.command.includes("post-tool-pr-hook")),
|
|
7973
|
+
{ command: shellCommand(postToolHookPath) }
|
|
7850
7974
|
];
|
|
7851
7975
|
await mkdir11(dirname5(path6), { recursive: true });
|
|
7852
7976
|
await writeFile6(path6, `${JSON.stringify({ ...config, version: 1, hooks }, null, 2)}
|
|
@@ -7865,7 +7989,12 @@ async function ensureKimiCommandProtectionHook() {
|
|
|
7865
7989
|
const block = `${start}
|
|
7866
7990
|
[[hooks]]
|
|
7867
7991
|
event = "PreToolUse"
|
|
7868
|
-
command = ${JSON.stringify(shellCommand("kimi"))}
|
|
7992
|
+
command = ${JSON.stringify(shellCommand(hookPath, "kimi"))}
|
|
7993
|
+
timeout = 30
|
|
7994
|
+
|
|
7995
|
+
[[hooks]]
|
|
7996
|
+
event = "PostToolUse"
|
|
7997
|
+
command = ${JSON.stringify(shellCommand(postToolHookPath))}
|
|
7869
7998
|
timeout = 30
|
|
7870
7999
|
${end}`;
|
|
7871
8000
|
const markerStart = config.indexOf(start);
|
|
@@ -9635,6 +9764,7 @@ var OpencodeManager = class extends CodingAgentManager {
|
|
|
9635
9764
|
await copyFile(OPENCODE_COMMAND_PROTECTION_PLUGIN_SOURCE, OPENCODE_COMMAND_PROTECTION_PLUGIN_PATH);
|
|
9636
9765
|
await rm2(OPENCODE_LEGACY_COMMAND_PROTECTION_PLUGIN_PATH, { force: true });
|
|
9637
9766
|
process.env.REPLICAS_COMMAND_PROTECTION_HOOK_PATH = getCommandProtectionHookPath();
|
|
9767
|
+
process.env.REPLICAS_POST_TOOL_PR_HOOK_PATH = getPostToolPrHookPath();
|
|
9638
9768
|
process.env.REPLICAS_COMMAND_PROTECTION_RUNTIME_PATH = process.execPath;
|
|
9639
9769
|
this.eventAbortController?.abort();
|
|
9640
9770
|
this.server?.close();
|
|
@@ -10022,6 +10152,12 @@ function registerCommandProtection(cwd, historyFile, recordHistoryEvent) {
|
|
|
10022
10152
|
}
|
|
10023
10153
|
return void 0;
|
|
10024
10154
|
});
|
|
10155
|
+
pi.on("tool_result", (event) => {
|
|
10156
|
+
if (!event.isError && mayCreatePullRequest(event.toolName, event.input)) {
|
|
10157
|
+
pullRequestCheckService.check().catch(() => {
|
|
10158
|
+
});
|
|
10159
|
+
}
|
|
10160
|
+
});
|
|
10025
10161
|
};
|
|
10026
10162
|
}
|
|
10027
10163
|
var PiManager = class extends CodingAgentManager {
|
|
@@ -12152,39 +12288,6 @@ async function uploadChatTranscript(chatId, filePath, chat, capture) {
|
|
|
12152
12288
|
return artifact;
|
|
12153
12289
|
}
|
|
12154
12290
|
|
|
12155
|
-
// src/services/upload-repo-state.ts
|
|
12156
|
-
var uploadQueue = Promise.resolve();
|
|
12157
|
-
async function sendRepoState(repos) {
|
|
12158
|
-
const form = new FormData();
|
|
12159
|
-
form.append(
|
|
12160
|
-
"file",
|
|
12161
|
-
new Blob([JSON.stringify({ repos })], { type: "application/json" }),
|
|
12162
|
-
"repo-state.json"
|
|
12163
|
-
);
|
|
12164
|
-
const response = await monolithRequest("/v1/engine/repo-state", { body: form });
|
|
12165
|
-
if (!response.ok) {
|
|
12166
|
-
const errorText = await response.text();
|
|
12167
|
-
throw new Error(`upload failed: ${response.status} ${errorText}`);
|
|
12168
|
-
}
|
|
12169
|
-
}
|
|
12170
|
-
function uploadRepoState(repos) {
|
|
12171
|
-
const upload = uploadQueue.then(() => sendRepoState(repos));
|
|
12172
|
-
uploadQueue = upload.catch(() => {
|
|
12173
|
-
});
|
|
12174
|
-
return upload;
|
|
12175
|
-
}
|
|
12176
|
-
async function flushRepoState() {
|
|
12177
|
-
try {
|
|
12178
|
-
const { repos, complete } = await gitService.listReposWithCompleteness({ includeDiffs: true });
|
|
12179
|
-
if (!complete) return { flushed: 0, skipped: 1, failed: 0 };
|
|
12180
|
-
await uploadRepoState(repos);
|
|
12181
|
-
return { flushed: 1, skipped: 0, failed: 0 };
|
|
12182
|
-
} catch (err) {
|
|
12183
|
-
console.error("[RepoStateUploader] upload failed:", err);
|
|
12184
|
-
return { flushed: 0, skipped: 0, failed: 1 };
|
|
12185
|
-
}
|
|
12186
|
-
}
|
|
12187
|
-
|
|
12188
12291
|
// src/services/upload-engine-logs.ts
|
|
12189
12292
|
import { createReadStream as createReadStream2 } from "fs";
|
|
12190
12293
|
import { readdir as readdir9, stat as stat5 } from "fs/promises";
|
|
@@ -13451,12 +13554,6 @@ var ChatService = class {
|
|
|
13451
13554
|
}
|
|
13452
13555
|
}
|
|
13453
13556
|
async publishAgentTurnCompleteWebhook(chat, completedAt, processing = false) {
|
|
13454
|
-
await Promise.all([
|
|
13455
|
-
githubTokenManager.refreshOnce().catch(() => {
|
|
13456
|
-
}),
|
|
13457
|
-
gitlabTokenManager.refreshOnce().catch(() => {
|
|
13458
|
-
})
|
|
13459
|
-
]);
|
|
13460
13557
|
const linearSessionId = ENGINE_ENV.REPLICAS_LINEAR_SESSION_ID;
|
|
13461
13558
|
const observedBranches = chat.observedBranchesByRepo;
|
|
13462
13559
|
chat.observedBranchesByRepo = /* @__PURE__ */ new Map();
|
|
@@ -13465,20 +13562,12 @@ var ChatService = class {
|
|
|
13465
13562
|
const errorNotificationTarget = chat.activeErrorNotificationTarget;
|
|
13466
13563
|
chat.activeErrorNotificationTarget = null;
|
|
13467
13564
|
let repoStatuses = [];
|
|
13468
|
-
let repoStateComplete = false;
|
|
13469
13565
|
try {
|
|
13470
|
-
|
|
13471
|
-
repoStatuses = refreshed.repos;
|
|
13472
|
-
repoStateComplete = refreshed.complete;
|
|
13566
|
+
repoStatuses = await refreshRepoStatuses(observedBranches, true);
|
|
13473
13567
|
console.log(`Repository Statuses Refreshed: `, repoStatuses);
|
|
13474
13568
|
} catch (error) {
|
|
13475
13569
|
console.error("[ChatService] Failed to refresh repo statuses:", error);
|
|
13476
13570
|
}
|
|
13477
|
-
if (repoStateComplete) {
|
|
13478
|
-
uploadRepoState(repoStatuses).catch((err) => {
|
|
13479
|
-
console.error("[ChatService] Failed to upload repository state:", err);
|
|
13480
|
-
});
|
|
13481
|
-
}
|
|
13482
13571
|
try {
|
|
13483
13572
|
const payload = {
|
|
13484
13573
|
...linearSessionId ? { linearSessionId } : {},
|
|
@@ -14319,6 +14408,11 @@ function jsonError(message, details) {
|
|
|
14319
14408
|
}
|
|
14320
14409
|
function createV1Routes(deps) {
|
|
14321
14410
|
const app2 = new Hono();
|
|
14411
|
+
app2.post("/pull-requests/check", (c) => {
|
|
14412
|
+
pullRequestCheckService.check().catch(() => {
|
|
14413
|
+
});
|
|
14414
|
+
return c.json({ accepted: true }, 202);
|
|
14415
|
+
});
|
|
14322
14416
|
const loadChatHistory = async (c, chatId, page, failureMessage, unexpectedStatus, legacy = false) => {
|
|
14323
14417
|
try {
|
|
14324
14418
|
const history = await deps.chatService.getChatHistory(chatId, legacy && page.limit !== void 0 ? {} : page);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
notifyPostToolUse
|
|
4
|
+
} from "./chunk-GDCMH7YA.js";
|
|
5
|
+
import {
|
|
6
|
+
isRecord
|
|
7
|
+
} from "./chunk-2RB7SIP3.js";
|
|
8
|
+
import "./chunk-WUJ4IALO.js";
|
|
9
|
+
|
|
10
|
+
// src/post-tool-pr-hook.ts
|
|
11
|
+
async function main() {
|
|
12
|
+
let stdin = "";
|
|
13
|
+
for await (const chunk of process.stdin) stdin += chunk;
|
|
14
|
+
try {
|
|
15
|
+
const input = JSON.parse(stdin);
|
|
16
|
+
const record = isRecord(input) ? input : {};
|
|
17
|
+
const toolName = [record.tool_name, record.tool, record.name].find((value) => typeof value === "string") ?? "";
|
|
18
|
+
const toolInput = record.tool_input ?? record.arguments ?? record.args ?? ("command" in record ? { command: record.command } : record);
|
|
19
|
+
await notifyPostToolUse(toolName, toolInput);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
22
|
+
`);
|
|
23
|
+
}
|
|
24
|
+
process.stdout.write("{}\n");
|
|
25
|
+
}
|
|
26
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "replicas-engine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.704",
|
|
4
4
|
"description": "Lightweight API server for Replicas workspaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"replicas-engine": "./dist/src/index.js",
|
|
9
9
|
"replicas-headless-agent": "./dist/src/headless-agent.js",
|
|
10
10
|
"replicas-command-protection-hook": "./dist/src/command-protection-hook.js",
|
|
11
|
+
"replicas-post-tool-pr-hook": "./dist/src/post-tool-pr-hook.js",
|
|
11
12
|
"replicas-engine-watchdog": "./scripts/engine-watchdog.sh"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
@@ -14,6 +14,8 @@ interface ToolExecuteBeforeOutput {
|
|
|
14
14
|
args: unknown;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
const toolInputs = new Map<string, unknown>();
|
|
18
|
+
|
|
17
19
|
function evaluate(tool: string, args: unknown, callId: string, cwd: string): Promise<PolicyResult> {
|
|
18
20
|
return new Promise((resolve) => {
|
|
19
21
|
let settled = false;
|
|
@@ -65,5 +67,22 @@ export const ReplicasCommandProtection = async ({ directory }: { directory: stri
|
|
|
65
67
|
'tool.execute.before': async (input: ToolExecuteBeforeInput, output: ToolExecuteBeforeOutput) => {
|
|
66
68
|
const result = await evaluate(input.tool, output.args, input.callID, directory);
|
|
67
69
|
if (!result.allowed) throw new Error(result.reason ?? 'Blocked by Replicas command protection.');
|
|
70
|
+
toolInputs.set(input.callID, output.args);
|
|
71
|
+
},
|
|
72
|
+
'tool.execute.after': async (input: ToolExecuteBeforeInput) => {
|
|
73
|
+
const args = toolInputs.get(input.callID);
|
|
74
|
+
toolInputs.delete(input.callID);
|
|
75
|
+
const hookPath = process.env.REPLICAS_POST_TOOL_PR_HOOK_PATH;
|
|
76
|
+
const runtimePath = process.env.REPLICAS_COMMAND_PROTECTION_RUNTIME_PATH;
|
|
77
|
+
if (!hookPath || !runtimePath) return;
|
|
78
|
+
const hookArgs = hookPath.endsWith('.ts') && !runtimePath.endsWith('/bun')
|
|
79
|
+
? ['--import', 'tsx', hookPath]
|
|
80
|
+
: [hookPath];
|
|
81
|
+
await new Promise<void>((resolve) => {
|
|
82
|
+
const child = spawn(runtimePath, hookArgs, { cwd: directory, stdio: ['pipe', 'ignore', 'ignore'] });
|
|
83
|
+
child.on('error', () => resolve());
|
|
84
|
+
child.on('exit', () => resolve());
|
|
85
|
+
child.stdin.end(JSON.stringify({ tool: input.tool, args }));
|
|
86
|
+
});
|
|
68
87
|
},
|
|
69
88
|
});
|