@akon-labs/nexus-cli 0.1.1 → 0.1.2

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 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAMnE,eAAO,MAAM,iBAAiB,EAAE,eAAe,EAI9C,CAAC","sourcesContent":["import type { InlineExtension } from \"../core/extensions/types.ts\";\nimport gitnexusExtension from \"./gitnexus/index.ts\";\nimport mcpExtension from \"./mcp/index.ts\";\n\n// llama.cpp support lives in ./llama and is kept in tree but not registered.\n// Re-add `{ name: \"llama.cpp\", factory: llamaExtension, hidden: true }` to turn it back on.\nexport const builtInExtensions: InlineExtension[] = [\n\t{ name: \"gitnexus\", factory: gitnexusExtension },\n\t// No-op until the user writes an mcp.json; connecting nothing costs nothing.\n\t{ name: \"mcp\", factory: mcpExtension },\n];\n"]}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAOnE,eAAO,MAAM,iBAAiB,EAAE,eAAe,EAK9C,CAAC","sourcesContent":["import type { InlineExtension } from \"../core/extensions/types.ts\";\nimport gitnexusExtension from \"./gitnexus/index.ts\";\nimport mcpExtension from \"./mcp/index.ts\";\nimport webSearchConcurrencyExtension from \"./web-search-concurrency.ts\";\n\n// llama.cpp support lives in ./llama and is kept in tree but not registered.\n// Re-add `{ name: \"llama.cpp\", factory: llamaExtension, hidden: true }` to turn it back on.\nexport const builtInExtensions: InlineExtension[] = [\n\t{ name: \"gitnexus\", factory: gitnexusExtension },\n\t// No-op until the user writes an mcp.json; connecting nothing costs nothing.\n\t{ name: \"mcp\", factory: mcpExtension },\n\t{ name: \"web-search-concurrency\", factory: webSearchConcurrencyExtension },\n];\n"]}
@@ -1,10 +1,12 @@
1
1
  import gitnexusExtension from "./gitnexus/index.js";
2
2
  import mcpExtension from "./mcp/index.js";
3
+ import webSearchConcurrencyExtension from "./web-search-concurrency.js";
3
4
  // llama.cpp support lives in ./llama and is kept in tree but not registered.
4
5
  // Re-add `{ name: "llama.cpp", factory: llamaExtension, hidden: true }` to turn it back on.
5
6
  export const builtInExtensions = [
6
7
  { name: "gitnexus", factory: gitnexusExtension },
7
8
  // No-op until the user writes an mcp.json; connecting nothing costs nothing.
8
9
  { name: "mcp", factory: mcpExtension },
10
+ { name: "web-search-concurrency", factory: webSearchConcurrencyExtension },
9
11
  ];
10
12
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AACA,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,YAAY,MAAM,gBAAgB,CAAC;AAE1C,6EAA6E;AAC7E,4FAA4F;AAC5F,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IACnD,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,EAAE;IAChD,6EAA6E;IAC7E,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE;CACtC,CAAC","sourcesContent":["import type { InlineExtension } from \"../core/extensions/types.ts\";\nimport gitnexusExtension from \"./gitnexus/index.ts\";\nimport mcpExtension from \"./mcp/index.ts\";\n\n// llama.cpp support lives in ./llama and is kept in tree but not registered.\n// Re-add `{ name: \"llama.cpp\", factory: llamaExtension, hidden: true }` to turn it back on.\nexport const builtInExtensions: InlineExtension[] = [\n\t{ name: \"gitnexus\", factory: gitnexusExtension },\n\t// No-op until the user writes an mcp.json; connecting nothing costs nothing.\n\t{ name: \"mcp\", factory: mcpExtension },\n];\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AACA,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,YAAY,MAAM,gBAAgB,CAAC;AAC1C,OAAO,6BAA6B,MAAM,6BAA6B,CAAC;AAExE,6EAA6E;AAC7E,4FAA4F;AAC5F,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IACnD,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,EAAE;IAChD,6EAA6E;IAC7E,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE;IACtC,EAAE,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,6BAA6B,EAAE;CAC1E,CAAC","sourcesContent":["import type { InlineExtension } from \"../core/extensions/types.ts\";\nimport gitnexusExtension from \"./gitnexus/index.ts\";\nimport mcpExtension from \"./mcp/index.ts\";\nimport webSearchConcurrencyExtension from \"./web-search-concurrency.ts\";\n\n// llama.cpp support lives in ./llama and is kept in tree but not registered.\n// Re-add `{ name: \"llama.cpp\", factory: llamaExtension, hidden: true }` to turn it back on.\nexport const builtInExtensions: InlineExtension[] = [\n\t{ name: \"gitnexus\", factory: gitnexusExtension },\n\t// No-op until the user writes an mcp.json; connecting nothing costs nothing.\n\t{ name: \"mcp\", factory: mcpExtension },\n\t{ name: \"web-search-concurrency\", factory: webSearchConcurrencyExtension },\n];\n"]}
@@ -0,0 +1,14 @@
1
+ export declare const WEB_ACCESS_EXTENSION_SOURCE = "npm:pi-web-access@0.22.0";
2
+ export declare const WEB_ACCESS_CONFIG_FILENAME = "web-search.json";
3
+ /**
4
+ * Point Pi ecosystem extensions at Nexus's config directory and add Nexus-safe
5
+ * defaults without replacing user-selected web access settings.
6
+ */
7
+ export declare function prepareWebAccessConfig(agentDir: string): {
8
+ changed: boolean;
9
+ warning?: string;
10
+ };
11
+ export declare function readConfiguredWebSearchToolName(agentDir: string): string | undefined;
12
+ /** Omit Nexus's default package without suppressing explicit -e paths. */
13
+ export declare function withDefaultWebAccessExtension(explicitExtensionPaths: string[] | undefined, omitDefaultExtension: boolean): string[] | undefined;
14
+ //# sourceMappingURL=web-access.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-access.d.ts","sourceRoot":"","sources":["../../src/extensions/web-access.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,2BAA2B,6BAA6B,CAAC;AACtE,eAAO,MAAM,0BAA0B,oBAAoB,CAAC;AAgC5D;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAqD/F;AAED,wBAAgB,+BAA+B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQpF;AAED,0EAA0E;AAC1E,wBAAgB,6BAA6B,CAC5C,sBAAsB,EAAE,MAAM,EAAE,GAAG,SAAS,EAC5C,oBAAoB,EAAE,OAAO,GAC3B,MAAM,EAAE,GAAG,SAAS,CAStB","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { setAppEnv } from \"../config.ts\";\n\nexport const WEB_ACCESS_EXTENSION_SOURCE = \"npm:pi-web-access@0.22.0\";\nexport const WEB_ACCESS_CONFIG_FILENAME = \"web-search.json\";\n\nconst DEFAULT_WORKFLOW = \"none\";\nconst DEFAULT_WEB_SEARCH_TOOL_NAME = \"websearch\";\nconst DEFAULT_SEARCH_ROUTING = {\n\tproviders: [\n\t\t\"searxng\",\n\t\t\"openai\",\n\t\t\"exa\",\n\t\t\"brave\",\n\t\t\"parallel\",\n\t\t\"tinyfish\",\n\t\t\"search1api\",\n\t\t\"searchinfinity\",\n\t\t\"querit\",\n\t\t\"tavily\",\n\t\t\"jina\",\n\t\t\"serpdive\",\n\t\t\"kagi\",\n\t\t\"bocha\",\n\t\t\"ollama\",\n\t\t\"perplexity\",\n\t\t\"gemini\",\n\t\t\"duckduckgo\",\n\t],\n\tfallbackOn: [\"quota\", \"transient\", \"network\", \"invalid-response\"],\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Point Pi ecosystem extensions at Nexus's config directory and add Nexus-safe\n * defaults without replacing user-selected web access settings.\n */\nexport function prepareWebAccessConfig(agentDir: string): { changed: boolean; warning?: string } {\n\tsetAppEnv(\"CODING_AGENT_DIR\", agentDir);\n\tconst configPath = join(agentDir, WEB_ACCESS_CONFIG_FILENAME);\n\n\tlet config: Record<string, unknown> = {};\n\tif (existsSync(configPath)) {\n\t\ttry {\n\t\t\tconst parsed: unknown = JSON.parse(readFileSync(configPath, \"utf-8\"));\n\t\t\tif (!isRecord(parsed)) {\n\t\t\t\treturn { changed: false };\n\t\t\t}\n\t\t\tconfig = parsed;\n\t\t} catch {\n\t\t\t// pi-web-access reports malformed config with its own path-aware diagnostic.\n\t\t\treturn { changed: false };\n\t\t}\n\t}\n\n\tlet changed = false;\n\tif (config.workflow === undefined) {\n\t\tconfig.workflow = DEFAULT_WORKFLOW;\n\t\tchanged = true;\n\t}\n\n\tif (config.toolNames === undefined) {\n\t\tconfig.toolNames = { webSearch: DEFAULT_WEB_SEARCH_TOOL_NAME };\n\t\tchanged = true;\n\t} else if (isRecord(config.toolNames) && config.toolNames.webSearch === undefined) {\n\t\tconfig.toolNames.webSearch = DEFAULT_WEB_SEARCH_TOOL_NAME;\n\t\tchanged = true;\n\t}\n\n\tconst hasExplicitProvider = config.provider !== undefined || config.searchProvider !== undefined;\n\tif (!hasExplicitProvider && config.searchRouting === undefined) {\n\t\tconfig.searchRouting = DEFAULT_SEARCH_ROUTING;\n\t\tchanged = true;\n\t}\n\n\tif (!changed) {\n\t\treturn { changed: false };\n\t}\n\n\ttry {\n\t\tmkdirSync(agentDir, { recursive: true });\n\t\twriteFileSync(configPath, `${JSON.stringify(config, null, 2)}\\n`, {\n\t\t\tencoding: \"utf-8\",\n\t\t\tmode: 0o600,\n\t\t});\n\t\treturn { changed: true };\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\treturn { changed: false, warning: `Could not write web access defaults to ${configPath}: ${message}` };\n\t}\n}\n\nexport function readConfiguredWebSearchToolName(agentDir: string): string | undefined {\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(readFileSync(join(agentDir, WEB_ACCESS_CONFIG_FILENAME), \"utf-8\"));\n\t\tif (!isRecord(parsed) || !isRecord(parsed.toolNames)) return undefined;\n\t\treturn typeof parsed.toolNames.webSearch === \"string\" ? parsed.toolNames.webSearch : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Omit Nexus's default package without suppressing explicit -e paths. */\nexport function withDefaultWebAccessExtension(\n\texplicitExtensionPaths: string[] | undefined,\n\tomitDefaultExtension: boolean,\n): string[] | undefined {\n\tif (omitDefaultExtension) {\n\t\treturn explicitExtensionPaths;\n\t}\n\n\treturn [\n\t\tWEB_ACCESS_EXTENSION_SOURCE,\n\t\t...(explicitExtensionPaths ?? []).filter((source) => source !== WEB_ACCESS_EXTENSION_SOURCE),\n\t];\n}\n"]}
@@ -0,0 +1,110 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { setAppEnv } from "../config.js";
4
+ export const WEB_ACCESS_EXTENSION_SOURCE = "npm:pi-web-access@0.22.0";
5
+ export const WEB_ACCESS_CONFIG_FILENAME = "web-search.json";
6
+ const DEFAULT_WORKFLOW = "none";
7
+ const DEFAULT_WEB_SEARCH_TOOL_NAME = "websearch";
8
+ const DEFAULT_SEARCH_ROUTING = {
9
+ providers: [
10
+ "searxng",
11
+ "openai",
12
+ "exa",
13
+ "brave",
14
+ "parallel",
15
+ "tinyfish",
16
+ "search1api",
17
+ "searchinfinity",
18
+ "querit",
19
+ "tavily",
20
+ "jina",
21
+ "serpdive",
22
+ "kagi",
23
+ "bocha",
24
+ "ollama",
25
+ "perplexity",
26
+ "gemini",
27
+ "duckduckgo",
28
+ ],
29
+ fallbackOn: ["quota", "transient", "network", "invalid-response"],
30
+ };
31
+ function isRecord(value) {
32
+ return typeof value === "object" && value !== null && !Array.isArray(value);
33
+ }
34
+ /**
35
+ * Point Pi ecosystem extensions at Nexus's config directory and add Nexus-safe
36
+ * defaults without replacing user-selected web access settings.
37
+ */
38
+ export function prepareWebAccessConfig(agentDir) {
39
+ setAppEnv("CODING_AGENT_DIR", agentDir);
40
+ const configPath = join(agentDir, WEB_ACCESS_CONFIG_FILENAME);
41
+ let config = {};
42
+ if (existsSync(configPath)) {
43
+ try {
44
+ const parsed = JSON.parse(readFileSync(configPath, "utf-8"));
45
+ if (!isRecord(parsed)) {
46
+ return { changed: false };
47
+ }
48
+ config = parsed;
49
+ }
50
+ catch {
51
+ // pi-web-access reports malformed config with its own path-aware diagnostic.
52
+ return { changed: false };
53
+ }
54
+ }
55
+ let changed = false;
56
+ if (config.workflow === undefined) {
57
+ config.workflow = DEFAULT_WORKFLOW;
58
+ changed = true;
59
+ }
60
+ if (config.toolNames === undefined) {
61
+ config.toolNames = { webSearch: DEFAULT_WEB_SEARCH_TOOL_NAME };
62
+ changed = true;
63
+ }
64
+ else if (isRecord(config.toolNames) && config.toolNames.webSearch === undefined) {
65
+ config.toolNames.webSearch = DEFAULT_WEB_SEARCH_TOOL_NAME;
66
+ changed = true;
67
+ }
68
+ const hasExplicitProvider = config.provider !== undefined || config.searchProvider !== undefined;
69
+ if (!hasExplicitProvider && config.searchRouting === undefined) {
70
+ config.searchRouting = DEFAULT_SEARCH_ROUTING;
71
+ changed = true;
72
+ }
73
+ if (!changed) {
74
+ return { changed: false };
75
+ }
76
+ try {
77
+ mkdirSync(agentDir, { recursive: true });
78
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, {
79
+ encoding: "utf-8",
80
+ mode: 0o600,
81
+ });
82
+ return { changed: true };
83
+ }
84
+ catch (error) {
85
+ const message = error instanceof Error ? error.message : String(error);
86
+ return { changed: false, warning: `Could not write web access defaults to ${configPath}: ${message}` };
87
+ }
88
+ }
89
+ export function readConfiguredWebSearchToolName(agentDir) {
90
+ try {
91
+ const parsed = JSON.parse(readFileSync(join(agentDir, WEB_ACCESS_CONFIG_FILENAME), "utf-8"));
92
+ if (!isRecord(parsed) || !isRecord(parsed.toolNames))
93
+ return undefined;
94
+ return typeof parsed.toolNames.webSearch === "string" ? parsed.toolNames.webSearch : undefined;
95
+ }
96
+ catch {
97
+ return undefined;
98
+ }
99
+ }
100
+ /** Omit Nexus's default package without suppressing explicit -e paths. */
101
+ export function withDefaultWebAccessExtension(explicitExtensionPaths, omitDefaultExtension) {
102
+ if (omitDefaultExtension) {
103
+ return explicitExtensionPaths;
104
+ }
105
+ return [
106
+ WEB_ACCESS_EXTENSION_SOURCE,
107
+ ...(explicitExtensionPaths ?? []).filter((source) => source !== WEB_ACCESS_EXTENSION_SOURCE),
108
+ ];
109
+ }
110
+ //# sourceMappingURL=web-access.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-access.js","sourceRoot":"","sources":["../../src/extensions/web-access.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { setAppEnv } from \"../config.ts\";\n\nexport const WEB_ACCESS_EXTENSION_SOURCE = \"npm:pi-web-access@0.22.0\";\nexport const WEB_ACCESS_CONFIG_FILENAME = \"web-search.json\";\n\nconst DEFAULT_WORKFLOW = \"none\";\nconst DEFAULT_WEB_SEARCH_TOOL_NAME = \"websearch\";\nconst DEFAULT_SEARCH_ROUTING = {\n\tproviders: [\n\t\t\"searxng\",\n\t\t\"openai\",\n\t\t\"exa\",\n\t\t\"brave\",\n\t\t\"parallel\",\n\t\t\"tinyfish\",\n\t\t\"search1api\",\n\t\t\"searchinfinity\",\n\t\t\"querit\",\n\t\t\"tavily\",\n\t\t\"jina\",\n\t\t\"serpdive\",\n\t\t\"kagi\",\n\t\t\"bocha\",\n\t\t\"ollama\",\n\t\t\"perplexity\",\n\t\t\"gemini\",\n\t\t\"duckduckgo\",\n\t],\n\tfallbackOn: [\"quota\", \"transient\", \"network\", \"invalid-response\"],\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Point Pi ecosystem extensions at Nexus's config directory and add Nexus-safe\n * defaults without replacing user-selected web access settings.\n */\nexport function prepareWebAccessConfig(agentDir: string): { changed: boolean; warning?: string } {\n\tsetAppEnv(\"CODING_AGENT_DIR\", agentDir);\n\tconst configPath = join(agentDir, WEB_ACCESS_CONFIG_FILENAME);\n\n\tlet config: Record<string, unknown> = {};\n\tif (existsSync(configPath)) {\n\t\ttry {\n\t\t\tconst parsed: unknown = JSON.parse(readFileSync(configPath, \"utf-8\"));\n\t\t\tif (!isRecord(parsed)) {\n\t\t\t\treturn { changed: false };\n\t\t\t}\n\t\t\tconfig = parsed;\n\t\t} catch {\n\t\t\t// pi-web-access reports malformed config with its own path-aware diagnostic.\n\t\t\treturn { changed: false };\n\t\t}\n\t}\n\n\tlet changed = false;\n\tif (config.workflow === undefined) {\n\t\tconfig.workflow = DEFAULT_WORKFLOW;\n\t\tchanged = true;\n\t}\n\n\tif (config.toolNames === undefined) {\n\t\tconfig.toolNames = { webSearch: DEFAULT_WEB_SEARCH_TOOL_NAME };\n\t\tchanged = true;\n\t} else if (isRecord(config.toolNames) && config.toolNames.webSearch === undefined) {\n\t\tconfig.toolNames.webSearch = DEFAULT_WEB_SEARCH_TOOL_NAME;\n\t\tchanged = true;\n\t}\n\n\tconst hasExplicitProvider = config.provider !== undefined || config.searchProvider !== undefined;\n\tif (!hasExplicitProvider && config.searchRouting === undefined) {\n\t\tconfig.searchRouting = DEFAULT_SEARCH_ROUTING;\n\t\tchanged = true;\n\t}\n\n\tif (!changed) {\n\t\treturn { changed: false };\n\t}\n\n\ttry {\n\t\tmkdirSync(agentDir, { recursive: true });\n\t\twriteFileSync(configPath, `${JSON.stringify(config, null, 2)}\\n`, {\n\t\t\tencoding: \"utf-8\",\n\t\t\tmode: 0o600,\n\t\t});\n\t\treturn { changed: true };\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\treturn { changed: false, warning: `Could not write web access defaults to ${configPath}: ${message}` };\n\t}\n}\n\nexport function readConfiguredWebSearchToolName(agentDir: string): string | undefined {\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(readFileSync(join(agentDir, WEB_ACCESS_CONFIG_FILENAME), \"utf-8\"));\n\t\tif (!isRecord(parsed) || !isRecord(parsed.toolNames)) return undefined;\n\t\treturn typeof parsed.toolNames.webSearch === \"string\" ? parsed.toolNames.webSearch : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Omit Nexus's default package without suppressing explicit -e paths. */\nexport function withDefaultWebAccessExtension(\n\texplicitExtensionPaths: string[] | undefined,\n\tomitDefaultExtension: boolean,\n): string[] | undefined {\n\tif (omitDefaultExtension) {\n\t\treturn explicitExtensionPaths;\n\t}\n\n\treturn [\n\t\tWEB_ACCESS_EXTENSION_SOURCE,\n\t\t...(explicitExtensionPaths ?? []).filter((source) => source !== WEB_ACCESS_EXTENSION_SOURCE),\n\t];\n}\n"]}
@@ -0,0 +1,14 @@
1
+ import type { ExtensionAPI } from "../core/extensions/types.ts";
2
+ export declare const MAX_CONCURRENT_WEB_SEARCHES = 3;
3
+ /** Small FIFO semaphore keyed by tool call ID. */
4
+ export declare class WebSearchConcurrencyLimiter {
5
+ private readonly active;
6
+ private readonly limit;
7
+ private readonly waiting;
8
+ constructor(limit: number);
9
+ acquire(id: string, signal?: AbortSignal): Promise<void>;
10
+ release(id: string): void;
11
+ cancelQueued(): void;
12
+ }
13
+ export default function webSearchConcurrencyExtension(pi: ExtensionAPI): void;
14
+ //# sourceMappingURL=web-search-concurrency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-search-concurrency.d.ts","sourceRoot":"","sources":["../../src/extensions/web-search-concurrency.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAGhE,eAAO,MAAM,2BAA2B,IAAI,CAAC;AAgB7C,kDAAkD;AAClD,qBAAa,2BAA2B;IACvC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;IAE/C,YAAY,KAAK,EAAE,MAAM,EAKxB;IAEK,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAyB7D;IAED,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAexB;IAED,YAAY,IAAI,IAAI,CAMnB;CACD;AAED,MAAM,CAAC,OAAO,UAAU,6BAA6B,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAmB5E","sourcesContent":["import { getAgentDir } from \"../config.ts\";\nimport type { ExtensionAPI } from \"../core/extensions/types.ts\";\nimport { readConfiguredWebSearchToolName } from \"./web-access.ts\";\n\nexport const MAX_CONCURRENT_WEB_SEARCHES = 3;\n\ninterface WaitingSearch {\n\tid: string;\n\tresolve: () => void;\n\treject: (error: Error) => void;\n\tsignal?: AbortSignal;\n\tonAbort?: () => void;\n}\n\nfunction abortError(): Error {\n\tconst error = new Error(\"Web search cancelled while waiting for a concurrency slot.\");\n\terror.name = \"AbortError\";\n\treturn error;\n}\n\n/** Small FIFO semaphore keyed by tool call ID. */\nexport class WebSearchConcurrencyLimiter {\n\tprivate readonly active = new Set<string>();\n\tprivate readonly limit: number;\n\tprivate readonly waiting: WaitingSearch[] = [];\n\n\tconstructor(limit: number) {\n\t\tif (!Number.isInteger(limit) || limit < 1) {\n\t\t\tthrow new Error(\"Web search concurrency limit must be a positive integer.\");\n\t\t}\n\t\tthis.limit = limit;\n\t}\n\n\tasync acquire(id: string, signal?: AbortSignal): Promise<void> {\n\t\tif (signal?.aborted) throw abortError();\n\n\t\tif (this.active.size < this.limit) {\n\t\t\tthis.active.add(id);\n\t\t\treturn;\n\t\t}\n\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tconst waiter: WaitingSearch = {\n\t\t\t\tid,\n\t\t\t\tresolve,\n\t\t\t\treject,\n\t\t\t\tsignal,\n\t\t\t};\n\t\t\tif (signal) {\n\t\t\t\twaiter.onAbort = () => {\n\t\t\t\t\tconst index = this.waiting.indexOf(waiter);\n\t\t\t\t\tif (index !== -1) this.waiting.splice(index, 1);\n\t\t\t\t\treject(abortError());\n\t\t\t\t};\n\t\t\t\tsignal.addEventListener(\"abort\", waiter.onAbort, { once: true });\n\t\t\t}\n\t\t\tthis.waiting.push(waiter);\n\t\t});\n\t}\n\n\trelease(id: string): void {\n\t\tif (!this.active.delete(id)) return;\n\n\t\twhile (this.waiting.length > 0) {\n\t\t\tconst waiter = this.waiting.shift();\n\t\t\tif (!waiter) return;\n\t\t\tif (waiter.onAbort) waiter.signal?.removeEventListener(\"abort\", waiter.onAbort);\n\t\t\tif (waiter.signal?.aborted) {\n\t\t\t\twaiter.reject(abortError());\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthis.active.add(waiter.id);\n\t\t\twaiter.resolve();\n\t\t\treturn;\n\t\t}\n\t}\n\n\tcancelQueued(): void {\n\t\tfor (const waiter of this.waiting.splice(0)) {\n\t\t\tif (waiter.onAbort) waiter.signal?.removeEventListener(\"abort\", waiter.onAbort);\n\t\t\twaiter.reject(abortError());\n\t\t}\n\t\tthis.active.clear();\n\t}\n}\n\nexport default function webSearchConcurrencyExtension(pi: ExtensionAPI): void {\n\tconst limiter = new WebSearchConcurrencyLimiter(MAX_CONCURRENT_WEB_SEARCHES);\n\tconst toolNames = new Set([\"websearch\", \"web_search\"]);\n\tconst configuredToolName = readConfiguredWebSearchToolName(getAgentDir());\n\tif (configuredToolName) toolNames.add(configuredToolName);\n\n\tpi.on(\"tool_call\", async (event, ctx) => {\n\t\tif (!toolNames.has(event.toolName)) return;\n\t\tawait limiter.acquire(event.toolCallId, ctx.signal);\n\t});\n\n\tpi.on(\"tool_execution_end\", (event) => {\n\t\tif (!toolNames.has(event.toolName)) return;\n\t\tlimiter.release(event.toolCallId);\n\t});\n\n\tpi.on(\"session_shutdown\", () => {\n\t\tlimiter.cancelQueued();\n\t});\n}\n"]}
@@ -0,0 +1,93 @@
1
+ import { getAgentDir } from "../config.js";
2
+ import { readConfiguredWebSearchToolName } from "./web-access.js";
3
+ export const MAX_CONCURRENT_WEB_SEARCHES = 3;
4
+ function abortError() {
5
+ const error = new Error("Web search cancelled while waiting for a concurrency slot.");
6
+ error.name = "AbortError";
7
+ return error;
8
+ }
9
+ /** Small FIFO semaphore keyed by tool call ID. */
10
+ export class WebSearchConcurrencyLimiter {
11
+ active = new Set();
12
+ limit;
13
+ waiting = [];
14
+ constructor(limit) {
15
+ if (!Number.isInteger(limit) || limit < 1) {
16
+ throw new Error("Web search concurrency limit must be a positive integer.");
17
+ }
18
+ this.limit = limit;
19
+ }
20
+ async acquire(id, signal) {
21
+ if (signal?.aborted)
22
+ throw abortError();
23
+ if (this.active.size < this.limit) {
24
+ this.active.add(id);
25
+ return;
26
+ }
27
+ await new Promise((resolve, reject) => {
28
+ const waiter = {
29
+ id,
30
+ resolve,
31
+ reject,
32
+ signal,
33
+ };
34
+ if (signal) {
35
+ waiter.onAbort = () => {
36
+ const index = this.waiting.indexOf(waiter);
37
+ if (index !== -1)
38
+ this.waiting.splice(index, 1);
39
+ reject(abortError());
40
+ };
41
+ signal.addEventListener("abort", waiter.onAbort, { once: true });
42
+ }
43
+ this.waiting.push(waiter);
44
+ });
45
+ }
46
+ release(id) {
47
+ if (!this.active.delete(id))
48
+ return;
49
+ while (this.waiting.length > 0) {
50
+ const waiter = this.waiting.shift();
51
+ if (!waiter)
52
+ return;
53
+ if (waiter.onAbort)
54
+ waiter.signal?.removeEventListener("abort", waiter.onAbort);
55
+ if (waiter.signal?.aborted) {
56
+ waiter.reject(abortError());
57
+ continue;
58
+ }
59
+ this.active.add(waiter.id);
60
+ waiter.resolve();
61
+ return;
62
+ }
63
+ }
64
+ cancelQueued() {
65
+ for (const waiter of this.waiting.splice(0)) {
66
+ if (waiter.onAbort)
67
+ waiter.signal?.removeEventListener("abort", waiter.onAbort);
68
+ waiter.reject(abortError());
69
+ }
70
+ this.active.clear();
71
+ }
72
+ }
73
+ export default function webSearchConcurrencyExtension(pi) {
74
+ const limiter = new WebSearchConcurrencyLimiter(MAX_CONCURRENT_WEB_SEARCHES);
75
+ const toolNames = new Set(["websearch", "web_search"]);
76
+ const configuredToolName = readConfiguredWebSearchToolName(getAgentDir());
77
+ if (configuredToolName)
78
+ toolNames.add(configuredToolName);
79
+ pi.on("tool_call", async (event, ctx) => {
80
+ if (!toolNames.has(event.toolName))
81
+ return;
82
+ await limiter.acquire(event.toolCallId, ctx.signal);
83
+ });
84
+ pi.on("tool_execution_end", (event) => {
85
+ if (!toolNames.has(event.toolName))
86
+ return;
87
+ limiter.release(event.toolCallId);
88
+ });
89
+ pi.on("session_shutdown", () => {
90
+ limiter.cancelQueued();
91
+ });
92
+ }
93
+ //# sourceMappingURL=web-search-concurrency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-search-concurrency.js","sourceRoot":"","sources":["../../src/extensions/web-search-concurrency.ts"],"names":[],"mappings":"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","sourcesContent":["import { getAgentDir } from \"../config.ts\";\nimport type { ExtensionAPI } from \"../core/extensions/types.ts\";\nimport { readConfiguredWebSearchToolName } from \"./web-access.ts\";\n\nexport const MAX_CONCURRENT_WEB_SEARCHES = 3;\n\ninterface WaitingSearch {\n\tid: string;\n\tresolve: () => void;\n\treject: (error: Error) => void;\n\tsignal?: AbortSignal;\n\tonAbort?: () => void;\n}\n\nfunction abortError(): Error {\n\tconst error = new Error(\"Web search cancelled while waiting for a concurrency slot.\");\n\terror.name = \"AbortError\";\n\treturn error;\n}\n\n/** Small FIFO semaphore keyed by tool call ID. */\nexport class WebSearchConcurrencyLimiter {\n\tprivate readonly active = new Set<string>();\n\tprivate readonly limit: number;\n\tprivate readonly waiting: WaitingSearch[] = [];\n\n\tconstructor(limit: number) {\n\t\tif (!Number.isInteger(limit) || limit < 1) {\n\t\t\tthrow new Error(\"Web search concurrency limit must be a positive integer.\");\n\t\t}\n\t\tthis.limit = limit;\n\t}\n\n\tasync acquire(id: string, signal?: AbortSignal): Promise<void> {\n\t\tif (signal?.aborted) throw abortError();\n\n\t\tif (this.active.size < this.limit) {\n\t\t\tthis.active.add(id);\n\t\t\treturn;\n\t\t}\n\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tconst waiter: WaitingSearch = {\n\t\t\t\tid,\n\t\t\t\tresolve,\n\t\t\t\treject,\n\t\t\t\tsignal,\n\t\t\t};\n\t\t\tif (signal) {\n\t\t\t\twaiter.onAbort = () => {\n\t\t\t\t\tconst index = this.waiting.indexOf(waiter);\n\t\t\t\t\tif (index !== -1) this.waiting.splice(index, 1);\n\t\t\t\t\treject(abortError());\n\t\t\t\t};\n\t\t\t\tsignal.addEventListener(\"abort\", waiter.onAbort, { once: true });\n\t\t\t}\n\t\t\tthis.waiting.push(waiter);\n\t\t});\n\t}\n\n\trelease(id: string): void {\n\t\tif (!this.active.delete(id)) return;\n\n\t\twhile (this.waiting.length > 0) {\n\t\t\tconst waiter = this.waiting.shift();\n\t\t\tif (!waiter) return;\n\t\t\tif (waiter.onAbort) waiter.signal?.removeEventListener(\"abort\", waiter.onAbort);\n\t\t\tif (waiter.signal?.aborted) {\n\t\t\t\twaiter.reject(abortError());\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthis.active.add(waiter.id);\n\t\t\twaiter.resolve();\n\t\t\treturn;\n\t\t}\n\t}\n\n\tcancelQueued(): void {\n\t\tfor (const waiter of this.waiting.splice(0)) {\n\t\t\tif (waiter.onAbort) waiter.signal?.removeEventListener(\"abort\", waiter.onAbort);\n\t\t\twaiter.reject(abortError());\n\t\t}\n\t\tthis.active.clear();\n\t}\n}\n\nexport default function webSearchConcurrencyExtension(pi: ExtensionAPI): void {\n\tconst limiter = new WebSearchConcurrencyLimiter(MAX_CONCURRENT_WEB_SEARCHES);\n\tconst toolNames = new Set([\"websearch\", \"web_search\"]);\n\tconst configuredToolName = readConfiguredWebSearchToolName(getAgentDir());\n\tif (configuredToolName) toolNames.add(configuredToolName);\n\n\tpi.on(\"tool_call\", async (event, ctx) => {\n\t\tif (!toolNames.has(event.toolName)) return;\n\t\tawait limiter.acquire(event.toolCallId, ctx.signal);\n\t});\n\n\tpi.on(\"tool_execution_end\", (event) => {\n\t\tif (!toolNames.has(event.toolName)) return;\n\t\tlimiter.release(event.toolCallId);\n\t});\n\n\tpi.on(\"session_shutdown\", () => {\n\t\tlimiter.cancelQueued();\n\t});\n}\n"]}