@singbox-iac/cli 0.1.0

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 (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +265 -0
  3. package/dist/cli/command-helpers.d.ts +6 -0
  4. package/dist/cli/command-helpers.js +34 -0
  5. package/dist/cli/command-helpers.js.map +1 -0
  6. package/dist/cli/commands/apply.d.ts +2 -0
  7. package/dist/cli/commands/apply.js +32 -0
  8. package/dist/cli/commands/apply.js.map +1 -0
  9. package/dist/cli/commands/author.d.ts +2 -0
  10. package/dist/cli/commands/author.js +194 -0
  11. package/dist/cli/commands/author.js.map +1 -0
  12. package/dist/cli/commands/build.d.ts +2 -0
  13. package/dist/cli/commands/build.js +198 -0
  14. package/dist/cli/commands/build.js.map +1 -0
  15. package/dist/cli/commands/check.d.ts +2 -0
  16. package/dist/cli/commands/check.js +23 -0
  17. package/dist/cli/commands/check.js.map +1 -0
  18. package/dist/cli/commands/doctor.d.ts +2 -0
  19. package/dist/cli/commands/doctor.js +35 -0
  20. package/dist/cli/commands/doctor.js.map +1 -0
  21. package/dist/cli/commands/init.d.ts +2 -0
  22. package/dist/cli/commands/init.js +23 -0
  23. package/dist/cli/commands/init.js.map +1 -0
  24. package/dist/cli/commands/reload.d.ts +2 -0
  25. package/dist/cli/commands/reload.js +21 -0
  26. package/dist/cli/commands/reload.js.map +1 -0
  27. package/dist/cli/commands/run.d.ts +2 -0
  28. package/dist/cli/commands/run.js +25 -0
  29. package/dist/cli/commands/run.js.map +1 -0
  30. package/dist/cli/commands/schedule.d.ts +2 -0
  31. package/dist/cli/commands/schedule.js +77 -0
  32. package/dist/cli/commands/schedule.js.map +1 -0
  33. package/dist/cli/commands/templates.d.ts +2 -0
  34. package/dist/cli/commands/templates.js +34 -0
  35. package/dist/cli/commands/templates.js.map +1 -0
  36. package/dist/cli/commands/update.d.ts +2 -0
  37. package/dist/cli/commands/update.js +52 -0
  38. package/dist/cli/commands/update.js.map +1 -0
  39. package/dist/cli/commands/verify.d.ts +2 -0
  40. package/dist/cli/commands/verify.js +49 -0
  41. package/dist/cli/commands/verify.js.map +1 -0
  42. package/dist/cli/index.d.ts +5 -0
  43. package/dist/cli/index.js +55 -0
  44. package/dist/cli/index.js.map +1 -0
  45. package/dist/config/load-config.d.ts +2 -0
  46. package/dist/config/load-config.js +29 -0
  47. package/dist/config/load-config.js.map +1 -0
  48. package/dist/config/schema.d.ts +548 -0
  49. package/dist/config/schema.js +92 -0
  50. package/dist/config/schema.js.map +1 -0
  51. package/dist/domain/config.d.ts +8 -0
  52. package/dist/domain/config.js +2 -0
  53. package/dist/domain/config.js.map +1 -0
  54. package/dist/domain/node.d.ts +11 -0
  55. package/dist/domain/node.js +2 -0
  56. package/dist/domain/node.js.map +1 -0
  57. package/dist/domain/outbound.d.ts +15 -0
  58. package/dist/domain/outbound.js +2 -0
  59. package/dist/domain/outbound.js.map +1 -0
  60. package/dist/domain/subscription.d.ts +6 -0
  61. package/dist/domain/subscription.js +2 -0
  62. package/dist/domain/subscription.js.map +1 -0
  63. package/dist/index.d.ts +1 -0
  64. package/dist/index.js +2 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/modules/authoring/index.d.ts +41 -0
  67. package/dist/modules/authoring/index.js +596 -0
  68. package/dist/modules/authoring/index.js.map +1 -0
  69. package/dist/modules/build/index.d.ts +13 -0
  70. package/dist/modules/build/index.js +39 -0
  71. package/dist/modules/build/index.js.map +1 -0
  72. package/dist/modules/compiler/index.d.ts +10 -0
  73. package/dist/modules/compiler/index.js +305 -0
  74. package/dist/modules/compiler/index.js.map +1 -0
  75. package/dist/modules/doctor/index.d.ts +17 -0
  76. package/dist/modules/doctor/index.js +89 -0
  77. package/dist/modules/doctor/index.js.map +1 -0
  78. package/dist/modules/fetcher/index.d.ts +4 -0
  79. package/dist/modules/fetcher/index.js +42 -0
  80. package/dist/modules/fetcher/index.js.map +1 -0
  81. package/dist/modules/init/index.d.ts +12 -0
  82. package/dist/modules/init/index.js +41 -0
  83. package/dist/modules/init/index.js.map +1 -0
  84. package/dist/modules/manager/index.d.ts +29 -0
  85. package/dist/modules/manager/index.js +133 -0
  86. package/dist/modules/manager/index.js.map +1 -0
  87. package/dist/modules/natural-language/index.d.ts +54 -0
  88. package/dist/modules/natural-language/index.js +458 -0
  89. package/dist/modules/natural-language/index.js.map +1 -0
  90. package/dist/modules/parser/index.d.ts +10 -0
  91. package/dist/modules/parser/index.js +113 -0
  92. package/dist/modules/parser/index.js.map +1 -0
  93. package/dist/modules/preview/index.d.ts +22 -0
  94. package/dist/modules/preview/index.js +141 -0
  95. package/dist/modules/preview/index.js.map +1 -0
  96. package/dist/modules/rule-templates/index.d.ts +15 -0
  97. package/dist/modules/rule-templates/index.js +200 -0
  98. package/dist/modules/rule-templates/index.js.map +1 -0
  99. package/dist/modules/schedule/index.d.ts +32 -0
  100. package/dist/modules/schedule/index.js +155 -0
  101. package/dist/modules/schedule/index.js.map +1 -0
  102. package/dist/modules/update/index.d.ts +22 -0
  103. package/dist/modules/update/index.js +38 -0
  104. package/dist/modules/update/index.js.map +1 -0
  105. package/dist/modules/user-rules/index.d.ts +18 -0
  106. package/dist/modules/user-rules/index.js +98 -0
  107. package/dist/modules/user-rules/index.js.map +1 -0
  108. package/dist/modules/verification/index.d.ts +49 -0
  109. package/dist/modules/verification/index.js +432 -0
  110. package/dist/modules/verification/index.js.map +1 -0
  111. package/dist/shared/errors.d.ts +3 -0
  112. package/dist/shared/errors.js +7 -0
  113. package/dist/shared/errors.js.map +1 -0
  114. package/dist/shared/logger.d.ts +2 -0
  115. package/dist/shared/logger.js +5 -0
  116. package/dist/shared/logger.js.map +1 -0
  117. package/dist/shared/result.d.ts +11 -0
  118. package/dist/shared/result.js +7 -0
  119. package/dist/shared/result.js.map +1 -0
  120. package/docs/antigravity-endpoints.md +77 -0
  121. package/docs/competitive-landscape.md +45 -0
  122. package/docs/development-workflow.md +80 -0
  123. package/docs/natural-language-authoring.md +376 -0
  124. package/docs/positioning.md +42 -0
  125. package/docs/releasing.md +72 -0
  126. package/docs/rule-templates.md +122 -0
  127. package/docs/rules-dsl.md +107 -0
  128. package/docs/runtime-on-macos.md +42 -0
  129. package/docs/sing-box-config-primer.md +39 -0
  130. package/docs/subscription-format.md +38 -0
  131. package/examples/builder.config.yaml +220 -0
  132. package/examples/custom.rules.yaml +18 -0
  133. package/package.json +51 -0
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=outbound.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbound.js","sourceRoot":"","sources":["../../src/domain/outbound.ts"],"names":[],"mappings":""}
@@ -0,0 +1,6 @@
1
+ export type SubscriptionFormat = "base64-lines";
2
+ export interface SubscriptionSource {
3
+ readonly url: string;
4
+ readonly format: SubscriptionFormat;
5
+ readonly protocols: readonly string[];
6
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=subscription.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscription.js","sourceRoot":"","sources":["../../src/domain/subscription.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ export * from "./cli/index.js";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./cli/index.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"}
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ import type { BuilderConfig } from "../../config/schema.js";
3
+ import { type NaturalLanguagePlan } from "../natural-language/index.js";
4
+ declare const providerSchema: z.ZodEnum<["deterministic", "auto", "claude", "exec"]>;
5
+ export type AuthoringProvider = z.infer<typeof providerSchema>;
6
+ export interface AuthoringCommandResult {
7
+ readonly exitCode: number;
8
+ readonly stdout: string;
9
+ readonly stderr: string;
10
+ readonly timedOut: boolean;
11
+ }
12
+ export interface LocalAiCli {
13
+ readonly id: string;
14
+ readonly command: string;
15
+ readonly path?: string;
16
+ readonly installed: boolean;
17
+ readonly authoringSupport: "builtin" | "exec" | "tooling-only";
18
+ }
19
+ export interface GenerateAuthoringPlanInput {
20
+ readonly prompt: string;
21
+ readonly config?: BuilderConfig;
22
+ readonly provider?: AuthoringProvider;
23
+ readonly execCommand?: string;
24
+ readonly execArgs?: readonly string[];
25
+ readonly timeoutMs?: number;
26
+ readonly runner?: AuthoringCommandRunner;
27
+ }
28
+ export interface GenerateAuthoringPlanResult {
29
+ readonly plan: NaturalLanguagePlan;
30
+ readonly providerRequested: AuthoringProvider;
31
+ readonly providerUsed: Exclude<AuthoringProvider, "auto">;
32
+ }
33
+ export type AuthoringCommandRunner = (input: {
34
+ readonly command: string;
35
+ readonly args: readonly string[];
36
+ readonly env?: NodeJS.ProcessEnv;
37
+ readonly timeoutMs: number;
38
+ }) => Promise<AuthoringCommandResult>;
39
+ export declare function generateAuthoringPlan(input: GenerateAuthoringPlanInput): Promise<GenerateAuthoringPlanResult>;
40
+ export declare function detectLocalAiClis(): Promise<readonly LocalAiCli[]>;
41
+ export {};
@@ -0,0 +1,596 @@
1
+ import { spawn } from "node:child_process";
2
+ import { constants } from "node:fs";
3
+ import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { z } from "zod";
7
+ import { generateRulesFromPrompt, } from "../natural-language/index.js";
8
+ import { mergeRuleTemplates } from "../rule-templates/index.js";
9
+ const providerSchema = z.enum(["deterministic", "auto", "claude", "exec"]);
10
+ const nonEmptyString = z.string().min(1);
11
+ const stringOrStringArray = z.union([nonEmptyString, z.array(nonEmptyString).min(1)]);
12
+ const authoringRuleSchema = z.object({
13
+ name: nonEmptyString.optional(),
14
+ inbound: stringOrStringArray.optional(),
15
+ protocol: nonEmptyString.optional(),
16
+ network: z.enum(["tcp", "udp"]).optional(),
17
+ port: z.number().int().positive().optional(),
18
+ domain: stringOrStringArray.optional(),
19
+ domainSuffix: stringOrStringArray.optional(),
20
+ ruleSet: stringOrStringArray.optional(),
21
+ route: nonEmptyString.optional(),
22
+ action: z.literal("reject").optional(),
23
+ });
24
+ const groupOverrideSchema = z.object({
25
+ defaultTarget: nonEmptyString.optional(),
26
+ defaultNodePattern: nonEmptyString.optional(),
27
+ });
28
+ const verificationOverrideSchema = z.object({
29
+ inbound: z.enum(["in-mixed", "in-proxifier"]).optional(),
30
+ domain: nonEmptyString.optional(),
31
+ domainSuffix: nonEmptyString.optional(),
32
+ expectedOutbound: nonEmptyString,
33
+ });
34
+ const authoredPlanSchema = z.object({
35
+ beforeBuiltins: z.array(authoringRuleSchema).default([]),
36
+ afterBuiltins: z.array(authoringRuleSchema).default([]),
37
+ templateIds: z.array(nonEmptyString).default([]),
38
+ notes: z.array(nonEmptyString).default([]),
39
+ scheduleIntervalMinutes: z.number().int().positive().optional(),
40
+ groupDefaults: z
41
+ .object({
42
+ processProxy: groupOverrideSchema.optional(),
43
+ aiOut: groupOverrideSchema.optional(),
44
+ devCommonOut: groupOverrideSchema.optional(),
45
+ stitchOut: groupOverrideSchema.optional(),
46
+ })
47
+ .default({}),
48
+ verificationOverrides: z.array(verificationOverrideSchema).default([]),
49
+ });
50
+ const defaultTimeoutMs = 4_000;
51
+ const planJsonSchema = JSON.stringify({
52
+ type: "object",
53
+ additionalProperties: false,
54
+ properties: {
55
+ beforeBuiltins: {
56
+ type: "array",
57
+ items: { type: "object" },
58
+ },
59
+ afterBuiltins: {
60
+ type: "array",
61
+ items: { type: "object" },
62
+ },
63
+ templateIds: {
64
+ type: "array",
65
+ items: { type: "string" },
66
+ },
67
+ notes: {
68
+ type: "array",
69
+ items: { type: "string" },
70
+ },
71
+ scheduleIntervalMinutes: { type: "integer", minimum: 1 },
72
+ groupDefaults: {
73
+ type: "object",
74
+ additionalProperties: false,
75
+ properties: {
76
+ processProxy: { type: "object" },
77
+ aiOut: { type: "object" },
78
+ devCommonOut: { type: "object" },
79
+ stitchOut: { type: "object" },
80
+ },
81
+ },
82
+ verificationOverrides: {
83
+ type: "array",
84
+ items: { type: "object" },
85
+ },
86
+ },
87
+ });
88
+ export async function generateAuthoringPlan(input) {
89
+ const providerRequested = input.provider ?? input.config?.authoring.provider ?? "deterministic";
90
+ const timeoutMs = input.timeoutMs ?? input.config?.authoring.timeoutMs ?? defaultTimeoutMs;
91
+ const runner = input.runner ?? runAuthoringCommand;
92
+ if (providerRequested === "deterministic") {
93
+ return {
94
+ providerRequested,
95
+ providerUsed: "deterministic",
96
+ plan: generateRulesFromPrompt(input.prompt),
97
+ };
98
+ }
99
+ if (providerRequested === "claude") {
100
+ return {
101
+ providerRequested,
102
+ providerUsed: "claude",
103
+ plan: await generateWithClaude(input.prompt, input.config, timeoutMs, runner),
104
+ };
105
+ }
106
+ if (providerRequested === "exec") {
107
+ return {
108
+ providerRequested,
109
+ providerUsed: "exec",
110
+ plan: await generateWithExec(input.prompt, input.config, {
111
+ command: input.execCommand ?? input.config?.authoring.exec?.command,
112
+ args: input.execArgs ?? input.config?.authoring.exec?.args,
113
+ }, timeoutMs, runner),
114
+ };
115
+ }
116
+ return generateWithAuto(input.prompt, input.config, input.execCommand, input.execArgs, timeoutMs, runner);
117
+ }
118
+ export async function detectLocalAiClis() {
119
+ const catalog = [
120
+ { id: "claude", command: "claude", authoringSupport: "builtin" },
121
+ { id: "gemini", command: "gemini", authoringSupport: "exec" },
122
+ { id: "codebuddy", command: "codebuddy", authoringSupport: "exec" },
123
+ { id: "cbc", command: "cbc", authoringSupport: "exec" },
124
+ { id: "opencode", command: "opencode", authoringSupport: "exec" },
125
+ { id: "codex", command: "codex", authoringSupport: "exec" },
126
+ { id: "qodercli", command: "qodercli", authoringSupport: "exec" },
127
+ { id: "qoder", command: "qoder", authoringSupport: "exec" },
128
+ { id: "qwen", command: "qwen", authoringSupport: "exec" },
129
+ { id: "trae", command: "trae", authoringSupport: "tooling-only" },
130
+ ];
131
+ const resolved = await Promise.all(catalog.map(async (entry) => {
132
+ const resolvedPath = await resolveExecutable(entry.command);
133
+ return {
134
+ ...entry,
135
+ installed: resolvedPath !== undefined,
136
+ ...(resolvedPath ? { path: resolvedPath } : {}),
137
+ };
138
+ }));
139
+ const dedupedByPath = new Map();
140
+ for (const entry of resolved) {
141
+ if (!entry.installed || !entry.path) {
142
+ dedupedByPath.set(`missing:${entry.id}`, entry);
143
+ continue;
144
+ }
145
+ const existing = dedupedByPath.get(entry.path);
146
+ if (!existing) {
147
+ dedupedByPath.set(entry.path, entry);
148
+ continue;
149
+ }
150
+ if (rankAuthoringSupport(entry.authoringSupport) > rankAuthoringSupport(existing.authoringSupport)) {
151
+ dedupedByPath.set(entry.path, entry);
152
+ }
153
+ }
154
+ return [...dedupedByPath.values()];
155
+ }
156
+ async function generateWithAuto(prompt, config, execCommand, execArgs, timeoutMs, runner) {
157
+ if (execCommand || config?.authoring.exec?.command) {
158
+ try {
159
+ return {
160
+ providerRequested: "auto",
161
+ providerUsed: "exec",
162
+ plan: await generateWithExec(prompt, config, {
163
+ command: execCommand ?? config?.authoring.exec?.command,
164
+ args: execArgs ?? config?.authoring.exec?.args,
165
+ }, timeoutMs, runner),
166
+ };
167
+ }
168
+ catch (error) {
169
+ return fallbackDeterministic(prompt, "exec", error instanceof Error ? error.message : String(error));
170
+ }
171
+ }
172
+ const claudePath = await resolveExecutable("claude");
173
+ if (claudePath) {
174
+ try {
175
+ return {
176
+ providerRequested: "auto",
177
+ providerUsed: "claude",
178
+ plan: await generateWithClaude(prompt, config, timeoutMs, runner, claudePath),
179
+ };
180
+ }
181
+ catch (error) {
182
+ return fallbackDeterministic(prompt, "claude", error instanceof Error ? error.message : String(error));
183
+ }
184
+ }
185
+ return {
186
+ providerRequested: "auto",
187
+ providerUsed: "deterministic",
188
+ plan: appendNotes(generateRulesFromPrompt(prompt), [
189
+ "Auto authoring fell back to the built-in deterministic parser because no supported local AI CLI was available.",
190
+ ]),
191
+ };
192
+ }
193
+ async function generateWithClaude(prompt, config, timeoutMs, runner, explicitCommand) {
194
+ const command = explicitCommand ?? (await resolveExecutable("claude"));
195
+ if (!command) {
196
+ throw new Error('The "claude" CLI was not found on PATH.');
197
+ }
198
+ const args = [
199
+ "-p",
200
+ "--tools",
201
+ "",
202
+ "--no-session-persistence",
203
+ "--permission-mode",
204
+ "dontAsk",
205
+ "--output-format",
206
+ "json",
207
+ "--json-schema",
208
+ planJsonSchema,
209
+ "--system-prompt",
210
+ buildAuthoringSystemPrompt(config),
211
+ prompt,
212
+ ];
213
+ const result = await runner({
214
+ command,
215
+ args,
216
+ timeoutMs,
217
+ });
218
+ if (result.timedOut) {
219
+ throw new Error(`The "claude" CLI did not produce a plan within ${timeoutMs} ms.`);
220
+ }
221
+ if (result.exitCode !== 0) {
222
+ throw new Error(result.stderr.trim() || result.stdout.trim() || '"claude" failed.');
223
+ }
224
+ return normalizeAuthoredPlan(JSON.parse(result.stdout));
225
+ }
226
+ async function generateWithExec(prompt, config, exec, timeoutMs, runner) {
227
+ if (!exec.command) {
228
+ throw new Error('The "exec" authoring provider requires a command.');
229
+ }
230
+ const command = (await resolveExecutable(exec.command)) ?? exec.command;
231
+ const contextJson = JSON.stringify(buildAuthoringContext(config));
232
+ const fullPrompt = buildPortableExecPrompt(config, prompt);
233
+ const rawArgs = exec.args ?? [];
234
+ const needsSchemaFile = rawArgs.some((value) => value.includes("{{schema_file}}"));
235
+ const needsOutputFile = rawArgs.some((value) => value.includes("{{output_file}}"));
236
+ const tempDir = needsSchemaFile || needsOutputFile
237
+ ? await mkdtemp(path.join(tmpdir(), "singbox-iac-author-exec-"))
238
+ : undefined;
239
+ const schemaFilePath = tempDir ? path.join(tempDir, "authoring.schema.json") : undefined;
240
+ const outputFilePath = tempDir ? path.join(tempDir, "authoring.output.json") : undefined;
241
+ try {
242
+ if (schemaFilePath && needsSchemaFile) {
243
+ await writeFile(schemaFilePath, planJsonSchema, "utf8");
244
+ }
245
+ const args = rawArgs.map((value) => value
246
+ .replaceAll("{{prompt}}", prompt)
247
+ .replaceAll("{{schema}}", planJsonSchema)
248
+ .replaceAll("{{context_json}}", contextJson)
249
+ .replaceAll("{{full_prompt}}", fullPrompt)
250
+ .replaceAll("{{schema_file}}", schemaFilePath ?? "")
251
+ .replaceAll("{{output_file}}", outputFilePath ?? ""));
252
+ const result = await runner({
253
+ command,
254
+ args,
255
+ timeoutMs,
256
+ env: {
257
+ ...process.env,
258
+ SINGBOX_IAC_AUTHOR_PROMPT: prompt,
259
+ SINGBOX_IAC_AUTHOR_SCHEMA: planJsonSchema,
260
+ SINGBOX_IAC_AUTHOR_CONTEXT: contextJson,
261
+ SINGBOX_IAC_AUTHOR_FULL_PROMPT: fullPrompt,
262
+ ...(schemaFilePath ? { SINGBOX_IAC_AUTHOR_SCHEMA_FILE: schemaFilePath } : {}),
263
+ ...(outputFilePath ? { SINGBOX_IAC_AUTHOR_OUTPUT_FILE: outputFilePath } : {}),
264
+ },
265
+ });
266
+ if (result.timedOut) {
267
+ throw new Error(`The exec authoring provider timed out after ${timeoutMs} ms.`);
268
+ }
269
+ if (result.exitCode !== 0) {
270
+ throw new Error(result.stderr.trim() || result.stdout.trim() || "exec provider failed.");
271
+ }
272
+ const rawOutput = outputFilePath ? await readFile(outputFilePath, "utf8") : result.stdout;
273
+ return normalizeAuthoredPlan(parseJsonObjectFromText(rawOutput));
274
+ }
275
+ finally {
276
+ if (tempDir) {
277
+ await rm(tempDir, { recursive: true, force: true });
278
+ }
279
+ }
280
+ }
281
+ function normalizeAuthoredPlan(input) {
282
+ const parsed = authoredPlanSchema.parse(input);
283
+ const mergedTemplates = mergeRuleTemplates(parsed.templateIds);
284
+ return {
285
+ beforeBuiltins: dedupeRules([
286
+ ...mergedTemplates.beforeBuiltins,
287
+ ...parsed.beforeBuiltins.map(normalizeRule),
288
+ ]),
289
+ afterBuiltins: dedupeRules([
290
+ ...mergedTemplates.afterBuiltins,
291
+ ...parsed.afterBuiltins.map(normalizeRule),
292
+ ]),
293
+ templateIds: parsed.templateIds,
294
+ notes: parsed.notes,
295
+ ...(parsed.scheduleIntervalMinutes
296
+ ? { scheduleIntervalMinutes: parsed.scheduleIntervalMinutes }
297
+ : {}),
298
+ ...(Object.keys(parsed.groupDefaults).length > 0
299
+ ? { groupDefaults: sanitizeGroupDefaults(parsed.groupDefaults) }
300
+ : {}),
301
+ ...(parsed.verificationOverrides.length > 0
302
+ ? { verificationOverrides: sanitizeVerificationOverrides(parsed.verificationOverrides) }
303
+ : {}),
304
+ };
305
+ }
306
+ function normalizeRule(rule) {
307
+ const normalized = {};
308
+ if (rule.name) {
309
+ normalized.name = rule.name;
310
+ }
311
+ if (rule.inbound) {
312
+ normalized.inbound = toArray(rule.inbound);
313
+ }
314
+ if (rule.protocol) {
315
+ normalized.protocol = rule.protocol;
316
+ }
317
+ if (rule.network) {
318
+ normalized.network = rule.network;
319
+ }
320
+ if (rule.port) {
321
+ normalized.port = rule.port;
322
+ }
323
+ if (rule.domain) {
324
+ normalized.domain = toArray(rule.domain);
325
+ }
326
+ if (rule.domainSuffix) {
327
+ normalized.domainSuffix = toArray(rule.domainSuffix);
328
+ }
329
+ if (rule.ruleSet) {
330
+ normalized.ruleSet = toArray(rule.ruleSet);
331
+ }
332
+ if (rule.route) {
333
+ normalized.route = rule.route;
334
+ }
335
+ if (rule.action) {
336
+ normalized.action = rule.action;
337
+ }
338
+ return normalized;
339
+ }
340
+ function toArray(value) {
341
+ if (Array.isArray(value)) {
342
+ return value;
343
+ }
344
+ return [value];
345
+ }
346
+ function dedupeRules(rules) {
347
+ const byKey = new Map();
348
+ for (const rule of rules) {
349
+ byKey.set(JSON.stringify(rule), rule);
350
+ }
351
+ return [...byKey.values()];
352
+ }
353
+ function buildAuthoringSystemPrompt(config) {
354
+ const context = buildAuthoringContext(config);
355
+ return [
356
+ "You translate short routing prompts into singbox-iac rule plans.",
357
+ "Respond only with JSON matching the supplied schema.",
358
+ "Use templateIds for common site bundles when possible.",
359
+ "Use beforeBuiltins for non-direct overrides and afterBuiltins for direct routing.",
360
+ "Do not invent process-matching rules for IDE or Proxifier phrases; emit a short note instead.",
361
+ `Allowed route targets: ${context.allowedRoutes.join(", ")}`,
362
+ `Available templateIds: ${context.templateIds.join(", ")}`,
363
+ `Available ruleSet tags: ${context.ruleSetTags.join(", ") || "(none)"}`,
364
+ ].join(" ");
365
+ }
366
+ function buildAuthoringContext(config) {
367
+ const allowedRoutes = new Set([
368
+ "direct",
369
+ "Global",
370
+ "Process-Proxy",
371
+ "AI-Out",
372
+ "Dev-Common-Out",
373
+ "Stitch-Out",
374
+ "HK",
375
+ "SG",
376
+ "US",
377
+ "JP",
378
+ ]);
379
+ if (config) {
380
+ for (const region of [
381
+ ...config.groups.global.includes,
382
+ ...config.groups.processProxy.includes,
383
+ ...config.groups.aiOut.includes,
384
+ ...config.groups.devCommonOut.includes,
385
+ ...config.groups.stitchOut.includes,
386
+ ]) {
387
+ allowedRoutes.add(region);
388
+ }
389
+ }
390
+ return {
391
+ allowedRoutes: [...allowedRoutes],
392
+ templateIds: [
393
+ "developer-ai-sites",
394
+ "developer-common-sites",
395
+ "video-us",
396
+ "video-hk",
397
+ "video-sg",
398
+ "video-jp",
399
+ "cn-video-direct",
400
+ ],
401
+ ruleSetTags: config?.ruleSets.map((ruleSet) => ruleSet.tag) ?? [],
402
+ };
403
+ }
404
+ function buildPortableExecPrompt(config, prompt) {
405
+ return [
406
+ "Translate the following routing request into a singbox-iac authoring plan.",
407
+ "Return only one JSON object and nothing else.",
408
+ "The JSON object must match this schema:",
409
+ planJsonSchema,
410
+ "Authoring context:",
411
+ JSON.stringify(buildAuthoringContext(config)),
412
+ "User request:",
413
+ prompt,
414
+ ].join("\n");
415
+ }
416
+ function fallbackDeterministic(prompt, providerUsed, reason) {
417
+ return {
418
+ providerRequested: "auto",
419
+ providerUsed: "deterministic",
420
+ plan: appendNotes(generateRulesFromPrompt(prompt), [
421
+ `Auto authoring fell back from ${providerUsed} to the deterministic parser: ${reason}`,
422
+ ]),
423
+ };
424
+ }
425
+ function appendNotes(plan, extraNotes) {
426
+ return {
427
+ ...plan,
428
+ notes: [...plan.notes, ...extraNotes],
429
+ };
430
+ }
431
+ function sanitizeGroupDefaults(groupDefaults) {
432
+ const sanitized = {};
433
+ for (const [key, value] of Object.entries(groupDefaults)) {
434
+ if (!value) {
435
+ continue;
436
+ }
437
+ const nextValue = {
438
+ ...(value.defaultTarget ? { defaultTarget: value.defaultTarget } : {}),
439
+ ...(value.defaultNodePattern ? { defaultNodePattern: value.defaultNodePattern } : {}),
440
+ };
441
+ if (Object.keys(nextValue).length === 0) {
442
+ continue;
443
+ }
444
+ sanitized[key] = nextValue;
445
+ }
446
+ return sanitized;
447
+ }
448
+ function sanitizeVerificationOverrides(overrides) {
449
+ return overrides.map((override) => ({
450
+ ...(override.inbound ? { inbound: override.inbound } : {}),
451
+ ...(override.domain ? { domain: override.domain } : {}),
452
+ ...(override.domainSuffix ? { domainSuffix: override.domainSuffix } : {}),
453
+ expectedOutbound: override.expectedOutbound,
454
+ }));
455
+ }
456
+ function rankAuthoringSupport(support) {
457
+ switch (support) {
458
+ case "builtin":
459
+ return 3;
460
+ case "exec":
461
+ return 2;
462
+ case "tooling-only":
463
+ return 1;
464
+ }
465
+ }
466
+ async function resolveExecutable(command) {
467
+ if (command.includes(path.sep)) {
468
+ return (await isExecutable(command)) ? command : undefined;
469
+ }
470
+ const pathValue = process.env.PATH ?? "";
471
+ for (const entry of pathValue.split(path.delimiter)) {
472
+ if (!entry) {
473
+ continue;
474
+ }
475
+ const candidate = path.join(entry, command);
476
+ if (await isExecutable(candidate)) {
477
+ return candidate;
478
+ }
479
+ }
480
+ return undefined;
481
+ }
482
+ async function isExecutable(filePath) {
483
+ try {
484
+ await access(filePath, constants.X_OK);
485
+ return true;
486
+ }
487
+ catch {
488
+ return false;
489
+ }
490
+ }
491
+ async function runAuthoringCommand(input) {
492
+ return new Promise((resolve, reject) => {
493
+ const child = spawn(input.command, input.args, {
494
+ env: input.env,
495
+ stdio: ["ignore", "pipe", "pipe"],
496
+ });
497
+ let stdout = "";
498
+ let stderr = "";
499
+ let timedOut = false;
500
+ const timer = setTimeout(() => {
501
+ timedOut = true;
502
+ child.kill("SIGTERM");
503
+ setTimeout(() => {
504
+ child.kill("SIGKILL");
505
+ }, 500).unref();
506
+ }, input.timeoutMs);
507
+ child.stdout.on("data", (chunk) => {
508
+ stdout += chunk.toString();
509
+ });
510
+ child.stderr.on("data", (chunk) => {
511
+ stderr += chunk.toString();
512
+ });
513
+ child.on("error", (error) => {
514
+ clearTimeout(timer);
515
+ reject(error);
516
+ });
517
+ child.on("close", (code) => {
518
+ clearTimeout(timer);
519
+ resolve({
520
+ exitCode: code ?? 0,
521
+ stdout: stdout.trim(),
522
+ stderr: stderr.trim(),
523
+ timedOut,
524
+ });
525
+ });
526
+ });
527
+ }
528
+ function parseJsonObjectFromText(rawText) {
529
+ const trimmed = rawText.trim();
530
+ if (trimmed.length === 0) {
531
+ throw new Error("Authoring provider returned empty output.");
532
+ }
533
+ try {
534
+ return JSON.parse(trimmed);
535
+ }
536
+ catch {
537
+ // continue
538
+ }
539
+ const fencedMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
540
+ if (fencedMatch?.[1]) {
541
+ const candidate = fencedMatch[1].trim();
542
+ try {
543
+ return JSON.parse(candidate);
544
+ }
545
+ catch {
546
+ // continue
547
+ }
548
+ }
549
+ const balanced = extractBalancedJsonObject(trimmed);
550
+ if (balanced) {
551
+ return JSON.parse(balanced);
552
+ }
553
+ throw new Error("Could not extract a JSON authoring plan from provider output.");
554
+ }
555
+ function extractBalancedJsonObject(text) {
556
+ let start = -1;
557
+ let depth = 0;
558
+ let inString = false;
559
+ let escaping = false;
560
+ for (let index = 0; index < text.length; index += 1) {
561
+ const character = text[index];
562
+ if (character === undefined) {
563
+ continue;
564
+ }
565
+ if (escaping) {
566
+ escaping = false;
567
+ continue;
568
+ }
569
+ if (character === "\\") {
570
+ escaping = true;
571
+ continue;
572
+ }
573
+ if (character === '"') {
574
+ inString = !inString;
575
+ continue;
576
+ }
577
+ if (inString) {
578
+ continue;
579
+ }
580
+ if (character === "{") {
581
+ if (depth === 0) {
582
+ start = index;
583
+ }
584
+ depth += 1;
585
+ continue;
586
+ }
587
+ if (character === "}" && depth > 0) {
588
+ depth -= 1;
589
+ if (depth === 0 && start >= 0) {
590
+ return text.slice(start, index + 1);
591
+ }
592
+ }
593
+ }
594
+ return undefined;
595
+ }
596
+ //# sourceMappingURL=index.js.map