@pushpalsdev/cli 1.0.17 → 1.0.19

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.
Files changed (106) hide show
  1. package/dist/pushpals-cli.js +542 -23
  2. package/package.json +1 -1
  3. package/runtime/sandbox/apps/workerpals/.python-version +1 -0
  4. package/runtime/sandbox/apps/workerpals/Dockerfile.sandbox +71 -0
  5. package/runtime/sandbox/apps/workerpals/package.json +25 -0
  6. package/runtime/sandbox/apps/workerpals/pyproject.toml +8 -0
  7. package/runtime/sandbox/apps/workerpals/src/backends/backend_config.ts +111 -0
  8. package/runtime/sandbox/apps/workerpals/src/backends/miniswe/miniswe_executor.py +2029 -0
  9. package/runtime/sandbox/apps/workerpals/src/backends/miniswe_backend.ts +48 -0
  10. package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/openai_codex_executor.py +1259 -0
  11. package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/test_openai_codex_runtime_config.py +110 -0
  12. package/runtime/sandbox/apps/workerpals/src/backends/openai_codex_backend.ts +67 -0
  13. package/runtime/sandbox/apps/workerpals/src/backends/openhands/openhands_executor.py +563 -0
  14. package/runtime/sandbox/apps/workerpals/src/backends/openhands_backend.ts +161 -0
  15. package/runtime/sandbox/apps/workerpals/src/backends/openhands_task_execute.ts +536 -0
  16. package/runtime/sandbox/apps/workerpals/src/backends/shared/executor_base.py +746 -0
  17. package/runtime/sandbox/apps/workerpals/src/backends/shared/test_settings_resolver.py +60 -0
  18. package/runtime/sandbox/apps/workerpals/src/backends/task_execute_registry.ts +21 -0
  19. package/runtime/sandbox/apps/workerpals/src/backends/types.ts +52 -0
  20. package/runtime/sandbox/apps/workerpals/src/common/execution_utils.ts +149 -0
  21. package/runtime/sandbox/apps/workerpals/src/common/executor_backend.ts +15 -0
  22. package/runtime/sandbox/apps/workerpals/src/common/generic_python_executor.ts +210 -0
  23. package/runtime/sandbox/apps/workerpals/src/common/logger.ts +65 -0
  24. package/runtime/sandbox/apps/workerpals/src/common/types.ts +9 -0
  25. package/runtime/sandbox/apps/workerpals/src/common/worktree_cleanup.ts +66 -0
  26. package/runtime/sandbox/apps/workerpals/src/context_manager.ts +45 -0
  27. package/runtime/sandbox/apps/workerpals/src/docker_executor.ts +1842 -0
  28. package/runtime/sandbox/apps/workerpals/src/execute_job.ts +3063 -0
  29. package/runtime/sandbox/apps/workerpals/src/job_runner.ts +194 -0
  30. package/runtime/sandbox/apps/workerpals/src/shell_manager.ts +210 -0
  31. package/runtime/sandbox/apps/workerpals/src/timeout_policy.ts +24 -0
  32. package/runtime/sandbox/apps/workerpals/src/workerpals_main.ts +1436 -0
  33. package/runtime/sandbox/apps/workerpals/tsconfig.json +15 -0
  34. package/runtime/sandbox/apps/workerpals/uv.lock +2014 -0
  35. package/runtime/sandbox/bun.lock +2591 -0
  36. package/runtime/sandbox/configs/backend.toml +79 -0
  37. package/runtime/sandbox/configs/default.toml +260 -0
  38. package/runtime/sandbox/configs/dev.toml +2 -0
  39. package/runtime/sandbox/configs/local.example.toml +129 -0
  40. package/runtime/sandbox/package.json +65 -0
  41. package/runtime/sandbox/packages/protocol/README.md +168 -0
  42. package/runtime/sandbox/packages/protocol/package.json +37 -0
  43. package/runtime/sandbox/packages/protocol/scripts/copy-schemas.js +17 -0
  44. package/runtime/sandbox/packages/protocol/src/a2a/README.md +52 -0
  45. package/runtime/sandbox/packages/protocol/src/a2a/mapping.ts +55 -0
  46. package/runtime/sandbox/packages/protocol/src/index.browser.ts +25 -0
  47. package/runtime/sandbox/packages/protocol/src/index.ts +25 -0
  48. package/runtime/sandbox/packages/protocol/src/schemas/approvals.schema.json +6 -0
  49. package/runtime/sandbox/packages/protocol/src/schemas/envelope.schema.json +96 -0
  50. package/runtime/sandbox/packages/protocol/src/schemas/events.schema.json +679 -0
  51. package/runtime/sandbox/packages/protocol/src/schemas/http.schema.json +50 -0
  52. package/runtime/sandbox/packages/protocol/src/types.ts +267 -0
  53. package/runtime/sandbox/packages/protocol/src/validate.browser.ts +154 -0
  54. package/runtime/sandbox/packages/protocol/src/validate.ts +233 -0
  55. package/runtime/sandbox/packages/protocol/src/version.ts +1 -0
  56. package/runtime/sandbox/packages/protocol/tsconfig.json +20 -0
  57. package/runtime/sandbox/packages/shared/package.json +19 -0
  58. package/runtime/sandbox/packages/shared/src/autonomy_policy.ts +400 -0
  59. package/runtime/sandbox/packages/shared/src/client_preflight.ts +297 -0
  60. package/runtime/sandbox/packages/shared/src/communication.ts +313 -0
  61. package/runtime/sandbox/packages/shared/src/config.ts +2201 -0
  62. package/runtime/sandbox/packages/shared/src/config_template_parity.ts +70 -0
  63. package/runtime/sandbox/packages/shared/src/git_backend.ts +205 -0
  64. package/runtime/sandbox/packages/shared/src/index.ts +100 -0
  65. package/runtime/sandbox/packages/shared/src/local_network.ts +101 -0
  66. package/runtime/sandbox/packages/shared/src/localbuddy_runtime.ts +329 -0
  67. package/runtime/sandbox/packages/shared/src/prompts.ts +64 -0
  68. package/runtime/sandbox/packages/shared/src/repo.ts +134 -0
  69. package/runtime/sandbox/packages/shared/src/session_event_visibility.ts +25 -0
  70. package/runtime/sandbox/packages/shared/src/vision.ts +247 -0
  71. package/runtime/sandbox/packages/shared/tsconfig.json +16 -0
  72. package/runtime/sandbox/prompts/workerpals/codex_quality_critic_instruction_prompt.md +14 -0
  73. package/runtime/sandbox/prompts/workerpals/commit_message_prompt.md +36 -0
  74. package/runtime/sandbox/prompts/workerpals/commit_message_user_prompt.md +7 -0
  75. package/runtime/sandbox/prompts/workerpals/miniswe_broker_system_prompt.md +33 -0
  76. package/runtime/sandbox/prompts/workerpals/miniswe_broker_task_prompt.md +5 -0
  77. package/runtime/sandbox/prompts/workerpals/miniswe_completion_requirement.md +1 -0
  78. package/runtime/sandbox/prompts/workerpals/miniswe_context_compaction_retry_prompt.md +1 -0
  79. package/runtime/sandbox/prompts/workerpals/miniswe_explicit_targets_block.md +2 -0
  80. package/runtime/sandbox/prompts/workerpals/miniswe_recovery_guidance_base.md +4 -0
  81. package/runtime/sandbox/prompts/workerpals/miniswe_recovery_guidance_blocker_line.md +1 -0
  82. package/runtime/sandbox/prompts/workerpals/miniswe_strict_tool_use_guidance.md +6 -0
  83. package/runtime/sandbox/prompts/workerpals/miniswe_supplemental_guidance_section.md +2 -0
  84. package/runtime/sandbox/prompts/workerpals/miniswe_timeout_note.md +1 -0
  85. package/runtime/sandbox/prompts/workerpals/miniswe_toolcall_retry_guidance.md +1 -0
  86. package/runtime/sandbox/prompts/workerpals/openai_codex_default_system_prompt.md +4 -0
  87. package/runtime/sandbox/prompts/workerpals/openai_codex_instruction_wrapper.md +5 -0
  88. package/runtime/sandbox/prompts/workerpals/openai_codex_runtime_policy_appendix.md +5 -0
  89. package/runtime/sandbox/prompts/workerpals/openai_codex_supplemental_guidance_section.md +2 -0
  90. package/runtime/sandbox/prompts/workerpals/openai_codex_task_execute_system_prompt.md +12 -0
  91. package/runtime/sandbox/prompts/workerpals/openhands_minimal_security_policy.j2 +8 -0
  92. package/runtime/sandbox/prompts/workerpals/openhands_minimal_system_prompt.j2 +20 -0
  93. package/runtime/sandbox/prompts/workerpals/openhands_strict_tool_use_message.md +1 -0
  94. package/runtime/sandbox/prompts/workerpals/openhands_supplemental_guidance_message.md +2 -0
  95. package/runtime/sandbox/prompts/workerpals/openhands_task_execute_fallback_system_prompt.md +1 -0
  96. package/runtime/sandbox/prompts/workerpals/openhands_task_execute_system_prompt.md +21 -0
  97. package/runtime/sandbox/prompts/workerpals/openhands_task_user_prompt.md +6 -0
  98. package/runtime/sandbox/prompts/workerpals/openhands_timeout_note.md +1 -0
  99. package/runtime/sandbox/prompts/workerpals/pr_description.md +42 -0
  100. package/runtime/sandbox/prompts/workerpals/task_quality_critic_system_prompt.md +9 -0
  101. package/runtime/sandbox/prompts/workerpals/task_quality_critic_user_prompt.md +17 -0
  102. package/runtime/sandbox/prompts/workerpals/workerpals_system_prompt.md +115 -0
  103. package/runtime/sandbox/protocol/schemas/approvals.schema.json +6 -0
  104. package/runtime/sandbox/protocol/schemas/envelope.schema.json +96 -0
  105. package/runtime/sandbox/protocol/schemas/events.schema.json +679 -0
  106. package/runtime/sandbox/protocol/schemas/http.schema.json +50 -0
@@ -0,0 +1,70 @@
1
+ import { readFileSync } from "fs";
2
+
3
+ function isObject(value: unknown): value is Record<string, unknown> {
4
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
5
+ }
6
+
7
+ export function collectDotEnvKeys(raw: string): Set<string> {
8
+ const keys = new Set<string>();
9
+ const lines = raw.split(/\r?\n/);
10
+ for (const line of lines) {
11
+ const trimmed = line.trim();
12
+ if (!trimmed || trimmed.startsWith("#")) continue;
13
+ const match = trimmed.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/);
14
+ if (!match) continue;
15
+ const key = match[1]?.trim();
16
+ if (key) keys.add(key);
17
+ }
18
+ return keys;
19
+ }
20
+
21
+ function collectTomlLeafKeysFromNode(
22
+ node: unknown,
23
+ prefix: string,
24
+ out: Set<string>,
25
+ ): void {
26
+ if (!isObject(node)) return;
27
+ for (const [rawKey, value] of Object.entries(node)) {
28
+ const key = rawKey.trim();
29
+ if (!key) continue;
30
+ const path = prefix ? `${prefix}.${key}` : key;
31
+ if (isObject(value)) {
32
+ collectTomlLeafKeysFromNode(value, path, out);
33
+ } else {
34
+ out.add(path);
35
+ }
36
+ }
37
+ }
38
+
39
+ export function collectTomlLeafKeys(raw: string): Set<string> {
40
+ const parsed = Bun.TOML.parse(raw) as unknown;
41
+ const keys = new Set<string>();
42
+ collectTomlLeafKeysFromNode(parsed, "", keys);
43
+ return keys;
44
+ }
45
+
46
+ export function missingTemplateKeys(templateKeys: Iterable<string>, localKeys: Set<string>): string[] {
47
+ const templateSet = new Set(templateKeys);
48
+ const missing: string[] = [];
49
+ for (const key of templateSet) {
50
+ if (!localKeys.has(key)) missing.push(key);
51
+ }
52
+ return missing.sort((a, b) => a.localeCompare(b));
53
+ }
54
+
55
+ export function extraLocalKeys(templateKeys: Iterable<string>, localKeys: Set<string>): string[] {
56
+ const templateSet = new Set(templateKeys);
57
+ const extras: string[] = [];
58
+ for (const key of localKeys) {
59
+ if (!templateSet.has(key)) extras.push(key);
60
+ }
61
+ return extras.sort((a, b) => a.localeCompare(b));
62
+ }
63
+
64
+ export function readDotEnvKeys(filePath: string): Set<string> {
65
+ return collectDotEnvKeys(readFileSync(filePath, "utf8"));
66
+ }
67
+
68
+ export function readTomlLeafKeys(filePath: string): Set<string> {
69
+ return collectTomlLeafKeys(readFileSync(filePath, "utf8"));
70
+ }
@@ -0,0 +1,205 @@
1
+ export type GitBackendId = "github" | "gitlab" | "unknown";
2
+ export type GitTokenSource = "configured" | "env" | "cli" | "none";
3
+
4
+ export interface CommandCaptureResult {
5
+ ok: boolean;
6
+ stdout: string;
7
+ stderr: string;
8
+ exitCode: number;
9
+ }
10
+
11
+ export interface ResolveGitTokenOptions {
12
+ remoteUrl: string;
13
+ configuredToken?: string | null;
14
+ env?: Record<string, string | undefined>;
15
+ cwd?: string;
16
+ runCommand?: (command: string[], cwd?: string) => Promise<CommandCaptureResult>;
17
+ }
18
+
19
+ export interface GitTokenResolution {
20
+ backend: GitBackendId;
21
+ host: string;
22
+ token: string;
23
+ source: GitTokenSource;
24
+ }
25
+
26
+ export interface GitHubRepoRef {
27
+ owner: string;
28
+ repo: string;
29
+ }
30
+
31
+ function trimToken(value: string | null | undefined): string {
32
+ return String(value ?? "").trim();
33
+ }
34
+
35
+ /**
36
+ * Remove userinfo credentials from HTTPS git remote URLs.
37
+ * Example: https://oauth2:token@github.com/org/repo.git -> https://github.com/org/repo.git
38
+ */
39
+ export function sanitizeGitRemoteUrl(remoteUrl: string): string {
40
+ const raw = trimToken(remoteUrl);
41
+ if (!raw) return "";
42
+ return raw.replace(/^(https?:\/\/)[^@/]+@/i, "$1");
43
+ }
44
+
45
+ function firstNonEmpty(
46
+ env: Record<string, string | undefined>,
47
+ keys: readonly string[],
48
+ ): string {
49
+ for (const key of keys) {
50
+ const value = trimToken(env[key]);
51
+ if (value) return value;
52
+ }
53
+ return "";
54
+ }
55
+
56
+ export function parseGitRemoteHost(remoteUrl: string): string {
57
+ const raw = trimToken(remoteUrl);
58
+ if (!raw) return "";
59
+
60
+ const patterns = [
61
+ /^https?:\/\/(?:[^@/]+@)?([^/:?#]+)(?::\d+)?(?:[/?#].*)?$/i,
62
+ /^ssh:\/\/(?:[^@/]+@)?([^/:?#]+)(?::\d+)?(?:[/?#].*)?$/i,
63
+ /^(?:[^@:\s]+@)?([^:/\s]+):[^?\s]+$/i,
64
+ ];
65
+
66
+ for (const pattern of patterns) {
67
+ const match = raw.match(pattern);
68
+ const host = match?.[1] ? trimToken(match[1]) : "";
69
+ if (host) return host.toLowerCase();
70
+ }
71
+ return "";
72
+ }
73
+
74
+ export function inferGitBackendFromRemote(remoteUrl: string): GitBackendId {
75
+ const host = parseGitRemoteHost(remoteUrl);
76
+ if (!host) return "unknown";
77
+ if (host === "github.com" || host.endsWith(".github.com") || host.includes("github")) {
78
+ return "github";
79
+ }
80
+ if (host === "gitlab.com" || host.endsWith(".gitlab.com") || host.includes("gitlab")) {
81
+ return "gitlab";
82
+ }
83
+ return "unknown";
84
+ }
85
+
86
+ export function parseGitHubRepo(remoteUrl: string): GitHubRepoRef | null {
87
+ const sanitized = sanitizeGitRemoteUrl(remoteUrl);
88
+ if (!sanitized) return null;
89
+
90
+ const httpsMatch = sanitized.match(
91
+ /^https?:\/\/(?:[^@/]+@)?github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?(?:[/?#].*)?$/i,
92
+ );
93
+ if (httpsMatch) {
94
+ return { owner: httpsMatch[1], repo: httpsMatch[2] };
95
+ }
96
+
97
+ const sshMatch = sanitized.match(
98
+ /^(?:ssh:\/\/)?(?:[^@/\s]+@)?github\.com[:/]([^/\s]+)\/([^/\s]+?)(?:\.git)?$/i,
99
+ );
100
+ if (sshMatch) {
101
+ return { owner: sshMatch[1], repo: sshMatch[2] };
102
+ }
103
+
104
+ return null;
105
+ }
106
+
107
+ export function toGitHubRepoWebUrl(remoteUrl: string): string | null {
108
+ const repo = parseGitHubRepo(remoteUrl);
109
+ if (!repo) return null;
110
+ return `https://github.com/${repo.owner}/${repo.repo}`;
111
+ }
112
+
113
+ async function defaultRunCommand(
114
+ command: string[],
115
+ cwd?: string,
116
+ ): Promise<CommandCaptureResult> {
117
+ try {
118
+ const proc = Bun.spawn(command, {
119
+ cwd,
120
+ stdout: "pipe",
121
+ stderr: "pipe",
122
+ });
123
+ const [stdout, stderr, exitCode] = await Promise.all([
124
+ new Response(proc.stdout).text(),
125
+ new Response(proc.stderr).text(),
126
+ proc.exited,
127
+ ]);
128
+ return {
129
+ ok: exitCode === 0,
130
+ stdout: stdout.trim(),
131
+ stderr: stderr.trim(),
132
+ exitCode,
133
+ };
134
+ } catch (err) {
135
+ return {
136
+ ok: false,
137
+ stdout: "",
138
+ stderr: String(err),
139
+ exitCode: 127,
140
+ };
141
+ }
142
+ }
143
+
144
+ async function resolveGitHubCliToken(
145
+ host: string,
146
+ runCommand: (command: string[], cwd?: string) => Promise<CommandCaptureResult>,
147
+ cwd?: string,
148
+ ): Promise<string> {
149
+ const useHostname = host && host !== "github.com";
150
+ const command = useHostname ? ["gh", "auth", "token", "--hostname", host] : ["gh", "auth", "token"];
151
+ const result = await runCommand(command, cwd);
152
+ return result.ok ? trimToken(result.stdout) : "";
153
+ }
154
+
155
+ async function resolveGitLabCliToken(
156
+ runCommand: (command: string[], cwd?: string) => Promise<CommandCaptureResult>,
157
+ cwd?: string,
158
+ ): Promise<string> {
159
+ const result = await runCommand(["glab", "auth", "token"], cwd);
160
+ return result.ok ? trimToken(result.stdout) : "";
161
+ }
162
+
163
+ export async function resolveGitTokenForRemote(
164
+ options: ResolveGitTokenOptions,
165
+ ): Promise<GitTokenResolution> {
166
+ const configuredToken = trimToken(options.configuredToken);
167
+ const host = parseGitRemoteHost(options.remoteUrl);
168
+ const backend = inferGitBackendFromRemote(options.remoteUrl);
169
+ const env = options.env ?? (process.env as Record<string, string | undefined>);
170
+
171
+ if (configuredToken) {
172
+ return { backend, host, token: configuredToken, source: "configured" };
173
+ }
174
+
175
+ const envVarOrder =
176
+ backend === "gitlab"
177
+ ? (["PUSHPALS_GIT_TOKEN", "GITLAB_TOKEN", "GL_TOKEN"] as const)
178
+ : backend === "github"
179
+ ? (["PUSHPALS_GIT_TOKEN", "GITHUB_TOKEN", "GH_TOKEN"] as const)
180
+ : (["PUSHPALS_GIT_TOKEN", "GITHUB_TOKEN", "GH_TOKEN", "GITLAB_TOKEN", "GL_TOKEN"] as const);
181
+
182
+ const envToken = firstNonEmpty(env, envVarOrder);
183
+ if (envToken) {
184
+ return { backend, host, token: envToken, source: "env" };
185
+ }
186
+
187
+ const runCommand = options.runCommand ?? defaultRunCommand;
188
+ let cliToken = "";
189
+ if (backend === "github") {
190
+ cliToken = await resolveGitHubCliToken(host, runCommand, options.cwd);
191
+ } else if (backend === "gitlab") {
192
+ cliToken = await resolveGitLabCliToken(runCommand, options.cwd);
193
+ } else {
194
+ // Unknown remotes still try both CLIs as a best-effort fallback.
195
+ cliToken = await resolveGitHubCliToken(host, runCommand, options.cwd);
196
+ if (!cliToken) {
197
+ cliToken = await resolveGitLabCliToken(runCommand, options.cwd);
198
+ }
199
+ }
200
+ if (cliToken) {
201
+ return { backend, host, token: cliToken, source: "cli" };
202
+ }
203
+
204
+ return { backend, host, token: "", source: "none" };
205
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Shared utilities package
3
+ */
4
+
5
+ export {
6
+ detectRepoRoot,
7
+ findGitRepoRoot,
8
+ getRepoContext,
9
+ resolveGitMetadataDir,
10
+ resolveGitStateFilePath,
11
+ } from "./repo.js";
12
+ export { CommunicationManager, type CommunicationManagerOptions } from "./communication.js";
13
+ export { loadPromptTemplate, loadRepoDocText } from "./prompts.js";
14
+ export {
15
+ evaluateClientRuntimePreflight,
16
+ formatClientRuntimePreflightLines,
17
+ type ClientPreflightCopyCommands,
18
+ type ClientPreflightIssue,
19
+ type ClientRuntimePreflightResult,
20
+ } from "./client_preflight.js";
21
+ export {
22
+ extractVisionKeyItems,
23
+ normalizeVisionSectionRef,
24
+ normalizeVisionSectionRefs,
25
+ parseVisionDoc,
26
+ validateVisionDocStructure,
27
+ type ParsedVisionDoc,
28
+ type VisionDocValidation,
29
+ type VisionKeyItems,
30
+ type VisionSection,
31
+ } from "./vision.js";
32
+ export {
33
+ inferGitBackendFromRemote,
34
+ parseGitHubRepo,
35
+ parseGitRemoteHost,
36
+ resolveGitTokenForRemote,
37
+ sanitizeGitRemoteUrl,
38
+ toGitHubRepoWebUrl,
39
+ type CommandCaptureResult,
40
+ type GitBackendId,
41
+ type GitHubRepoRef,
42
+ type GitTokenResolution,
43
+ type GitTokenSource,
44
+ type ResolveGitTokenOptions,
45
+ } from "./git_backend.js";
46
+ export {
47
+ invalidatePushPalsConfigCache,
48
+ loadPushPalsConfig,
49
+ sanitizePushPalsConfigForLogging,
50
+ type PushPalsConfig,
51
+ type PushPalsLlmConfig,
52
+ type PushPalsLmStudioConfig,
53
+ } from "./config.js";
54
+ export {
55
+ buildLocalCorsHeaders,
56
+ isLoopbackOrigin,
57
+ isLoopbackHost,
58
+ normalizeLoopbackHost,
59
+ normalizeLoopbackHttpUrl,
60
+ resolveLocalServerConnection,
61
+ } from "./local_network.js";
62
+ export {
63
+ isHeartbeatStatusSessionEvent,
64
+ shouldDisplayInteractiveSessionEvent,
65
+ } from "./session_event_visibility.js";
66
+ export {
67
+ DEFAULT_LOCALBUDDY_PORT,
68
+ computeLocalBuddyRestartBackoffMs,
69
+ loadLocalBuddyRuntimeSnapshotFromFiles,
70
+ parseLocalBuddyRuntimeSnapshot,
71
+ resolveLocalBuddyRuntimeAction,
72
+ resolveLocalBuddyStartGate,
73
+ type LocalBuddyRuntimeAction,
74
+ type LocalBuddyRuntimeSnapshot,
75
+ type LocalBuddyStartGateReason,
76
+ } from "./localbuddy_runtime.js";
77
+ export {
78
+ classifyGlobBreadth,
79
+ componentRootPrefix,
80
+ containsGlobMeta,
81
+ deriveAutonomyComponentArea,
82
+ globBreadthScore,
83
+ literalPrefix,
84
+ makePatternKey,
85
+ matchesGlob,
86
+ normalizeAutonomyComponentArea,
87
+ normalizePenalties,
88
+ normalizeRepoRelativePath,
89
+ normalizeTargetPath,
90
+ normalizeWriteGlob,
91
+ penaltyTotal,
92
+ validateScopeInvariants,
93
+ type AutonomyComponentArea,
94
+ type AutonomyGlobBreadth,
95
+ type AutonomyObjectiveType,
96
+ type AutonomyPenalty,
97
+ type AutonomyPenaltyKind,
98
+ type AutonomyRiskLevel,
99
+ type ScopeValidationResult,
100
+ } from "./autonomy_policy.js";
@@ -0,0 +1,101 @@
1
+ const DEFAULT_LOCAL_LOOPBACK_HOST = "127.0.0.1";
2
+
3
+ export function isLoopbackHost(hostname: string): boolean {
4
+ const normalized = String(hostname ?? "")
5
+ .trim()
6
+ .toLowerCase()
7
+ .replace(/^\[(.*)\]$/, "$1");
8
+ return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
9
+ }
10
+
11
+ export function normalizeLoopbackHost(hostname: string | null | undefined): string {
12
+ const normalized = String(hostname ?? "")
13
+ .trim()
14
+ .toLowerCase()
15
+ .replace(/^\[(.*)\]$/, "$1");
16
+ if (isLoopbackHost(normalized)) return DEFAULT_LOCAL_LOOPBACK_HOST;
17
+ return DEFAULT_LOCAL_LOOPBACK_HOST;
18
+ }
19
+
20
+ export function isLoopbackOrigin(origin: string | null | undefined): boolean {
21
+ const text = String(origin ?? "").trim();
22
+ if (!text) return false;
23
+ try {
24
+ const parsed = new URL(text);
25
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
26
+ return false;
27
+ }
28
+ return isLoopbackHost(parsed.hostname);
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ export function buildLocalCorsHeaders(options: {
35
+ origin: string | null | undefined;
36
+ allowAuthorizationHeader?: boolean;
37
+ }): Record<string, string> {
38
+ const headers: Record<string, string> = {
39
+ "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
40
+ "Access-Control-Allow-Headers": options.allowAuthorizationHeader
41
+ ? "content-type, authorization"
42
+ : "content-type",
43
+ };
44
+ const origin = String(options.origin ?? "").trim();
45
+ if (isLoopbackOrigin(origin)) {
46
+ headers["Access-Control-Allow-Origin"] = origin;
47
+ headers["Vary"] = "Origin";
48
+ }
49
+ return headers;
50
+ }
51
+
52
+ export function normalizeLoopbackHttpUrl(
53
+ value: string | null | undefined,
54
+ fallbackPort: number,
55
+ ): string {
56
+ const fallback = `http://${DEFAULT_LOCAL_LOOPBACK_HOST}:${Math.max(1, fallbackPort)}`;
57
+ const text = String(value ?? "").trim();
58
+ if (!text) return fallback;
59
+
60
+ try {
61
+ const parsed = new URL(text);
62
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
63
+ return fallback;
64
+ }
65
+ parsed.protocol = "http:";
66
+ parsed.username = "";
67
+ parsed.password = "";
68
+ parsed.hostname = normalizeLoopbackHost(parsed.hostname);
69
+ // Force server endpoints to the local root path in local-only mode.
70
+ parsed.pathname = "/";
71
+ parsed.search = "";
72
+ parsed.hash = "";
73
+ if (!parsed.port) {
74
+ parsed.port = String(Math.max(1, fallbackPort));
75
+ }
76
+ return parsed.toString().replace(/\/+$/, "");
77
+ } catch {
78
+ return fallback;
79
+ }
80
+ }
81
+
82
+ export function resolveLocalServerConnection(options: {
83
+ serverUrl: string | null | undefined;
84
+ authToken: string | null | undefined;
85
+ fallbackPort: number;
86
+ }): {
87
+ serverUrl: string;
88
+ authToken: null;
89
+ serverWasNormalized: boolean;
90
+ authTokenWasIgnored: boolean;
91
+ } {
92
+ const rawServer = String(options.serverUrl ?? "").trim().replace(/\/+$/, "");
93
+ const normalizedServer = normalizeLoopbackHttpUrl(rawServer, options.fallbackPort);
94
+ const authToken = String(options.authToken ?? "").trim();
95
+ return {
96
+ serverUrl: normalizedServer,
97
+ authToken: null,
98
+ serverWasNormalized: !!rawServer && normalizedServer !== rawServer,
99
+ authTokenWasIgnored: authToken.length > 0,
100
+ };
101
+ }