@code-yeongyu/senpi 2026.10.9 → 2026.10.10-3

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 (113) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/bundle/chunks/{anthropic-messages-CVXI6CLT.js → anthropic-messages-OE3QBX4P.js} +1 -1
  3. package/dist/bundle/chunks/{app-server-command-2HNDZGTR.js → app-server-command-DVWM37GF.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-MJGHZT4N.js → chunk-3A3C5VVO.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-Q2ZTPMWD.js → chunk-4TK7VCZZ.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-CB77NQMN.js → chunk-7GVD54RM.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-6EH7QXOG.js → chunk-CTERKXVZ.js} +2 -2
  8. package/dist/bundle/chunks/{chunk-SB22X6ZR.js → chunk-H4UIUFU5.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-CM5QTRC2.js → chunk-H67EVDWG.js} +1 -1
  10. package/dist/bundle/chunks/chunk-I5HLTDT4.js +2 -0
  11. package/dist/bundle/chunks/chunk-IZOTKUWG.js +2 -0
  12. package/dist/bundle/chunks/{chunk-DERNLWDS.js → chunk-MT2C4I5R.js} +151 -148
  13. package/dist/bundle/chunks/{chunk-LSE7KE2C.js → chunk-ODYQK5XJ.js} +1 -1
  14. package/dist/bundle/chunks/chunk-PDSG6VER.js +14 -0
  15. package/dist/bundle/chunks/{chunk-LUWWM7GA.js → chunk-RADHRQ4K.js} +3 -3
  16. package/dist/bundle/chunks/chunk-RRU5LXMS.js +3 -0
  17. package/dist/bundle/chunks/{chunk-7Q3EY6HH.js → chunk-UMJL74VN.js} +1 -1
  18. package/dist/bundle/chunks/{chunk-3YXB7Z7O.js → chunk-Z7MMKKIY.js} +1 -1
  19. package/dist/bundle/chunks/{cli-main-VMDAGZCA.js → cli-main-B4S4DGBX.js} +1 -1
  20. package/dist/bundle/chunks/{help-fast-path-JBQLYO7W.js → help-fast-path-EHRZEBRO.js} +1 -1
  21. package/dist/bundle/chunks/{host-command-HTYYQBSG.js → host-command-VATE7HRG.js} +1 -1
  22. package/dist/bundle/chunks/{host-lifecycle-XZF6IKKO.js → host-lifecycle-GC3GIR2N.js} +1 -1
  23. package/dist/bundle/chunks/{interactive-mode-U7IH4VMJ.js → interactive-mode-QOFA64JL.js} +1 -1
  24. package/dist/bundle/chunks/{multi-session-host-HHIC6N7C.js → multi-session-host-E5RDPWQN.js} +1 -1
  25. package/dist/bundle/chunks/{openai-completions-6HU7GIGD.js → openai-completions-Q2LEWXEC.js} +1 -1
  26. package/dist/bundle/chunks/{openai-responses-S4QYZXF3.js → openai-responses-DHGWSTZM.js} +1 -1
  27. package/dist/bundle/chunks/{package-manager-cli-2FXWPM2U.js → package-manager-cli-YGTWXRG5.js} +1 -1
  28. package/dist/bundle/chunks/{rotation-stream-ZJQ6I2RZ.js → rotation-stream-C5BM7TMM.js} +1 -1
  29. package/dist/bundle/chunks/{rpc-mode-K5YGBGQO.js → rpc-mode-HDAGQ4X3.js} +1 -1
  30. package/dist/bundle/chunks/{schedule-command-CVAPK2WQ.js → schedule-command-EQ4PQCRP.js} +1 -1
  31. package/dist/bundle/chunks/{session-control-endpoint-THEF3K7M.js → session-control-endpoint-NABTZHCZ.js} +2 -2
  32. package/dist/bundle/chunks/{session-picker-R5N2EKUS.js → session-picker-O7OESJWU.js} +1 -1
  33. package/dist/bundle/chunks/session-worker.js +70 -67
  34. package/dist/bundle/cli.js +1 -1
  35. package/dist/bundle/index.js +1 -1
  36. package/dist/bundle/rpc-entry.js +1 -1
  37. package/dist/bundle/runtime-manifest.json +1 -1
  38. package/dist/core/agent-session-runtime.d.ts +9 -0
  39. package/dist/core/agent-session-runtime.js +22 -0
  40. package/dist/core/agent-session.d.ts +14 -1
  41. package/dist/core/agent-session.js +56 -8
  42. package/dist/core/extensions/builtin/btw/panel.d.ts +3 -1
  43. package/dist/core/extensions/builtin/btw/panel.js +14 -9
  44. package/dist/core/extensions/builtin/compaction/context-pipeline.d.ts +5 -0
  45. package/dist/core/extensions/builtin/compaction/context-pipeline.js +1 -0
  46. package/dist/core/extensions/builtin/compaction/index.js +2 -1
  47. package/dist/core/extensions/builtin/compaction/lane-policy.d.ts +7 -0
  48. package/dist/core/extensions/builtin/compaction/lane-policy.js +28 -15
  49. package/dist/core/extensions/builtin/compaction/log.d.ts +2 -0
  50. package/dist/core/extensions/builtin/compaction/log.js +2 -1
  51. package/dist/core/extensions/builtin/goal/cache-warm-renderer.js +14 -8
  52. package/dist/core/extensions/builtin/goal/cache-warm.d.ts +4 -2
  53. package/dist/core/extensions/builtin/goal/lifecycle-helpers.js +1 -1
  54. package/dist/core/extensions/builtin/goal/monitor-continuation.js +9 -1
  55. package/dist/core/extensions/builtin/goal/prompt.d.ts +6 -1
  56. package/dist/core/extensions/builtin/goal/prompt.js +32 -1
  57. package/dist/core/extensions/builtin/mcp/commands.js +21 -26
  58. package/dist/core/extensions/builtin/mcp/config-edit.d.ts +2 -0
  59. package/dist/core/extensions/builtin/mcp/config-edit.js +5 -2
  60. package/dist/core/extensions/builtin/mcp/manager-view.d.ts +34 -0
  61. package/dist/core/extensions/builtin/mcp/manager-view.js +136 -0
  62. package/dist/core/extensions/builtin/mcp/manager.d.ts +8 -0
  63. package/dist/core/extensions/builtin/mcp/manager.js +193 -0
  64. package/dist/core/extensions/builtin/mcp/service.d.ts +3 -1
  65. package/dist/core/extensions/builtin/mcp/service.js +4 -0
  66. package/dist/core/extensions/builtin/rules/rules/finder.js +8 -3
  67. package/dist/core/extensions/builtin/websearch/websearch/config.js +2 -0
  68. package/dist/core/extensions/builtin/websearch/websearch/provider-endpoints.js +1 -0
  69. package/dist/core/extensions/builtin/websearch/websearch/providers/keenable.d.ts +3 -0
  70. package/dist/core/extensions/builtin/websearch/websearch/providers/keenable.js +55 -0
  71. package/dist/core/extensions/builtin/websearch/websearch/providers.js +2 -0
  72. package/dist/core/extensions/builtin/websearch/websearch/types.d.ts +1 -1
  73. package/dist/core/model-config-schema.d.ts +5 -0
  74. package/dist/core/model-config-schema.js +1 -0
  75. package/dist/core/provider-composer.js +5 -1
  76. package/dist/core/provider-timeout-retry.d.ts +7 -1
  77. package/dist/core/provider-timeout-retry.js +23 -12
  78. package/dist/core/reload-veto.d.ts +1 -1
  79. package/dist/core/reload-veto.js +7 -1
  80. package/dist/core/retry-fallback/expansion.js +9 -3
  81. package/dist/core/sdk.js +7 -3
  82. package/dist/core/thinking-levels.d.ts +23 -1
  83. package/dist/core/thinking-levels.js +37 -0
  84. package/dist/core/trust-manager.js +50 -0
  85. package/dist/main.js +12 -13
  86. package/dist/modes/interactive/interactive-mode.js +8 -0
  87. package/dist/modes/interactive/session-control-endpoint.js +33 -8
  88. package/dist/modes/interactive/session-control-registry.js +3 -1
  89. package/dist/modes/interactive/session-control-wake.d.ts +11 -5
  90. package/dist/modes/interactive/session-control-wake.js +38 -16
  91. package/dist/modes/rpc/connection-handler.d.ts +5 -0
  92. package/dist/modes/rpc/connection-handler.js +30 -3
  93. package/dist/modes/rpc/custom-capability.d.ts +6 -0
  94. package/dist/modes/rpc/custom-capability.js +6 -0
  95. package/dist/modes/rpc/host-daemon-paths.d.ts +5 -4
  96. package/dist/modes/rpc/host-daemon-paths.js +17 -9
  97. package/dist/modes/rpc/host-daemon-registration.d.ts +6 -1
  98. package/dist/modes/rpc/host-daemon-registration.js +6 -2
  99. package/dist/modes/rpc/host-session-control.js +6 -4
  100. package/dist/modes/rpc/rpc-client.d.ts +5 -0
  101. package/dist/modes/rpc/rpc-client.js +7 -0
  102. package/dist/modes/rpc/rpc-mode.js +1 -1
  103. package/dist/modes/rpc/rpc-types.d.ts +9 -0
  104. package/docs/json.md +3 -0
  105. package/docs/mcp.md +16 -1
  106. package/docs/models.md +34 -0
  107. package/docs/rpc.md +36 -1
  108. package/docs/web-search.md +3 -1
  109. package/package.json +6 -6
  110. package/dist/bundle/chunks/chunk-6SMU7ANE.js +0 -3
  111. package/dist/bundle/chunks/chunk-M3BQ2PAQ.js +0 -14
  112. package/dist/bundle/chunks/chunk-VJJ53ORU.js +0 -2
  113. package/dist/bundle/chunks/chunk-YSPHACIN.js +0 -2
@@ -0,0 +1,193 @@
1
+ import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
+ import { resolveAuthMode } from "./auth/context.js";
3
+ import { updateMcpServerConfig } from "./config-edit.js";
4
+ import { collectAllPages } from "./expose/pagination.js";
5
+ import { McpManagerView } from "./manager-view.js";
6
+ import { buildMcpStatusRows, formatMcpStatus } from "./status.js";
7
+ const EXPOSURES = ["auto", "direct", "search", "proxy"];
8
+ export async function showMcpManager(ctx, pi, service, run) {
9
+ const authAction = await ctx.ui.custom((tui, theme, keybindings, done) => {
10
+ const view = new McpManagerView(tui, theme, keybindings);
11
+ void manage(view, ctx, pi, service, run).then(done, (error) => {
12
+ done(undefined);
13
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
14
+ });
15
+ return view;
16
+ });
17
+ // The existing auth flow retains notices in the transcript; do not hide it behind a custom view.
18
+ if (authAction)
19
+ await run(authAction.command, authAction.name, ctx);
20
+ }
21
+ async function manage(view, ctx, pi, service, run) {
22
+ const subscribe = (listener) => {
23
+ let disconnect = [];
24
+ const bind = () => {
25
+ for (const stop of disconnect)
26
+ stop();
27
+ disconnect = service.getServerSnapshots().flatMap((snapshot) => {
28
+ const connection = service.getConnection(snapshot.name);
29
+ return connection ? [connection.onStateChange(listener)] : [];
30
+ });
31
+ };
32
+ bind();
33
+ const unregister = service.onMcpRegistrationChanged(() => {
34
+ bind();
35
+ listener();
36
+ });
37
+ return () => {
38
+ unregister();
39
+ for (const stop of disconnect)
40
+ stop();
41
+ };
42
+ };
43
+ for (;;) {
44
+ const name = await view.menu(async () => {
45
+ const rows = await buildMcpStatusRows(service.getServerSnapshots(), (server) => service.getServerExposureStatus(server));
46
+ return {
47
+ title: "MCP servers",
48
+ items: rows.map(({ snapshot, exposure }) => {
49
+ const resources = service.getMcpResourceServers(pi).find((server) => server.server === snapshot.name);
50
+ return {
51
+ value: snapshot.name,
52
+ label: snapshot.name,
53
+ description: [
54
+ snapshot.configState === "enabled" ? snapshot.lifecycleState : snapshot.configState,
55
+ `${exposure.toolCount ?? "?"} tools`,
56
+ ...(resources ? [`${resources.resources.length} resources`] : []),
57
+ exposure.mode ?? service.getAuthTarget(snapshot.name)?.config.exposure ?? "auto",
58
+ snapshot.source ?? "unknown",
59
+ ].join(" · "),
60
+ };
61
+ }),
62
+ empty: "No MCP servers configured. Add one with /mcp add <name> <command...|url>.",
63
+ confirmLabel: "manage",
64
+ cancelLabel: "close",
65
+ };
66
+ }, subscribe);
67
+ if (name === undefined)
68
+ return undefined;
69
+ let message;
70
+ for (;;) {
71
+ const action = await view.menu(() => {
72
+ const snapshot = service.getServerSnapshots().find((server) => server.name === name);
73
+ const config = service.getAuthTarget(name)?.config;
74
+ return {
75
+ title: `MCP server ${name}`,
76
+ details: [snapshot?.lastError, message].filter(Boolean).join("\n") || undefined,
77
+ items: snapshot ? serverActions(snapshot, config, service.getServerAuthStatus(name) === "oAuth") : [],
78
+ empty: "This server is no longer configured.",
79
+ confirmLabel: "select",
80
+ cancelLabel: "back",
81
+ };
82
+ }, subscribe);
83
+ if (action === undefined)
84
+ break;
85
+ const snapshot = service.getServerSnapshots().find((server) => server.name === name);
86
+ if (!snapshot)
87
+ break;
88
+ if (action === "auth" || action === "logout")
89
+ return { command: action, name };
90
+ message = undefined;
91
+ try {
92
+ if (action === "status") {
93
+ const rows = await buildMcpStatusRows([snapshot], (server) => service.getServerExposureStatus(server));
94
+ await showText(view, `Details of ${name}`, formatMcpStatus("", rows).trim());
95
+ }
96
+ else if (action === "logs") {
97
+ await showText(view, `Logs of ${name}`, service.getLogLines(name, 20).join("\n") || "(empty)");
98
+ }
99
+ else if (action === "tools") {
100
+ const connection = service.getConnection(name);
101
+ if (connection?.state !== "connected")
102
+ continue;
103
+ const tools = await collectAllPages((cursor) => connection.client.listTools(cursor === undefined ? {} : { cursor }, { timeout: 2000 }));
104
+ await view.menu(() => ({
105
+ title: `Tools of ${name}`,
106
+ items: tools.items.map((tool) => ({
107
+ value: tool.name,
108
+ label: tool.name,
109
+ description: tool.description,
110
+ })),
111
+ empty: "The server offers no tools.",
112
+ confirmLabel: "back",
113
+ cancelLabel: "back",
114
+ }));
115
+ }
116
+ else if (action === "enable" || action === "disable" || action === "exposure") {
117
+ let exposure;
118
+ if (action === "exposure") {
119
+ const choice = await view.menu(() => ({
120
+ title: `Exposure of ${name}`,
121
+ items: EXPOSURES.map((value) => ({ value, label: value })),
122
+ selected: service.getAuthTarget(name)?.config.exposure,
123
+ confirmLabel: "save",
124
+ cancelLabel: "back",
125
+ }));
126
+ exposure = EXPOSURES.find((value) => value === choice);
127
+ if (exposure === undefined)
128
+ continue;
129
+ }
130
+ if (!isWritable(snapshot) || !snapshot.sourcePath)
131
+ continue;
132
+ const patch = exposure === undefined ? { enabled: action === "enable" } : { exposure };
133
+ if (!updateMcpServerConfig(snapshot.sourcePath, name, patch)) {
134
+ message = "This server is no longer in its configuration file.";
135
+ continue;
136
+ }
137
+ view.status(`MCP server ${name}`, "Applying configuration...");
138
+ await service.attachSession({ type: "session_start", reason: "reload" }, ctx, pi);
139
+ message = "Configuration saved.";
140
+ }
141
+ else if (action === "test" || action === "reconnect") {
142
+ view.status(`MCP server ${name}`, action === "test" ? "Testing..." : "Reconnecting...");
143
+ const notices = [];
144
+ await run(action, name, ctx, (text) => notices.push(text));
145
+ message = notices.join("\n");
146
+ }
147
+ }
148
+ catch (error) {
149
+ message = error instanceof Error ? error.message : String(error);
150
+ }
151
+ }
152
+ }
153
+ }
154
+ function isWritable(snapshot) {
155
+ return snapshot.configState !== "untrusted" && (snapshot.source === "global" || snapshot.source === "project");
156
+ }
157
+ function serverActions(snapshot, config, loggedIn) {
158
+ const items = [
159
+ { value: "status", label: "Details" },
160
+ { value: "logs", label: "Logs" },
161
+ ];
162
+ if (snapshot.configState === "enabled") {
163
+ if (snapshot.lifecycleState === "connected")
164
+ items.push({ value: "tools", label: "Tools" });
165
+ items.push({ value: "test", label: "Test connection" }, { value: "reconnect", label: "Reconnect" });
166
+ if (config && resolveAuthMode(config) === "oauth") {
167
+ items.push(loggedIn
168
+ ? { value: "logout", label: "Sign out", description: "deletes stored credentials" }
169
+ : { value: "auth", label: "Sign in", description: "closes this view and opens authorization" });
170
+ }
171
+ }
172
+ if (isWritable(snapshot)) {
173
+ items.push({
174
+ value: snapshot.configState === "disabled" ? "enable" : "disable",
175
+ label: snapshot.configState === "disabled" ? "Enable" : "Disable",
176
+ description: `saved to ${snapshot.source} mcp.json`,
177
+ });
178
+ items.push({ value: "exposure", label: "Exposure", description: config?.exposure });
179
+ }
180
+ return items;
181
+ }
182
+ async function showText(view, title, text) {
183
+ await view.menu(() => ({
184
+ title,
185
+ items: wrapTextWithAnsi(text, Math.max(1, view.columns - 4)).map((label, index) => ({
186
+ value: String(index),
187
+ label,
188
+ })),
189
+ confirmLabel: "back",
190
+ cancelLabel: "back",
191
+ }));
192
+ }
193
+ //# sourceMappingURL=manager.js.map
@@ -4,7 +4,7 @@ import type { ServerConnection } from "./connection.ts";
4
4
  import type { McpServerExposureStatus } from "./expose/status.ts";
5
5
  import { McpOutputArtifacts } from "./guard/output-guard.ts";
6
6
  import type { McpResourceServer } from "./resources.ts";
7
- import type { McpDisposeReason, McpServerSnapshot, McpServiceSnapshot, McpSessionContext, McpSessionOptions, McpWireStatusSnapshot } from "./service-types.ts";
7
+ import type { McpDisposeReason, McpServerSnapshot, McpServiceSnapshot, McpSessionContext, McpSessionOptions, McpWireAuthStatus, McpWireStatusSnapshot } from "./service-types.ts";
8
8
  import type { SkillServerRegistration } from "./skills.ts";
9
9
  import { type McpStartupRaceResult } from "./startup-race.ts";
10
10
  type McpElicitationUi = Pick<ExtensionUIContext, "input" | "select" | "confirm">;
@@ -97,6 +97,8 @@ export declare class McpService {
97
97
  env?: Record<string, string | undefined>;
98
98
  callbackUrl?: string;
99
99
  } | undefined;
100
+ /** Live auth status without fetching catalogs or exposing credentials. */
101
+ getServerAuthStatus(name: string): McpWireAuthStatus;
100
102
  getCachedInstructions(name: string): string | undefined;
101
103
  /** Resolved `settings.nativeToolSearch` (auto | true | false | undefined).
102
104
  * Drives the native provider tool-search adapter gate. */
@@ -688,6 +688,10 @@ export class McpService {
688
688
  callbackUrl: this.#config?.settings.oauthCallbackUrl,
689
689
  };
690
690
  }
691
+ /** Live auth status without fetching catalogs or exposing credentials. */
692
+ getServerAuthStatus(name) {
693
+ return wireAuthStatus(this.#entryForName(name), this.#config?.servers[name]);
694
+ }
691
695
  getCachedInstructions(name) {
692
696
  return this.#entryForName(name)?.cachedCatalog?.instructions;
693
697
  }
@@ -1,6 +1,6 @@
1
1
  import { existsSync, realpathSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { dirname, join, posix, relative, resolve } from "node:path";
3
+ import { dirname, isAbsolute, join, posix, relative, resolve } from "node:path";
4
4
  import { GLOBAL_DISTANCE, PROJECT_RULE_SUBDIRS, PROJECT_SINGLE_FILES, USER_HOME_RULE_SUBDIRS, USER_HOME_SINGLE_FILES, } from "./constants.js";
5
5
  import { UnsupportedRuleSourceError } from "./errors.js";
6
6
  import { scanRuleFiles } from "./scanner.js";
@@ -151,7 +151,9 @@ function getWalkDirectories(projectRoot, targetFile) {
151
151
  let distance = 0;
152
152
  while (true) {
153
153
  walkDirectories.push({ directory: currentDirectory, distance });
154
- if (currentDirectory === projectRoot) {
154
+ // Windows drive letters are case-insensitive, so an exact string compare misses
155
+ // "c:\proj" against a "C:\proj" root and walks past it; relative() folds the case.
156
+ if (relative(projectRoot, currentDirectory) === "") {
155
157
  break;
156
158
  }
157
159
  const parentDirectory = dirname(currentDirectory);
@@ -165,7 +167,10 @@ function getWalkDirectories(projectRoot, targetFile) {
165
167
  }
166
168
  function isSameOrChildPath(childPath, parentPath) {
167
169
  const childRelativePath = relative(parentPath, childPath);
168
- return childRelativePath === "" || (!childRelativePath.startsWith("..") && !childRelativePath.startsWith("/"));
170
+ // Cross-drive / UNC: relative() between two Windows roots returns an absolute
171
+ // path (e.g. "D:\other"), which starts with neither ".." nor "/", so a
172
+ // startsWith("/") test would accept a target outside the project root.
173
+ return childRelativePath === "" || (!childRelativePath.startsWith("..") && !isAbsolute(childRelativePath));
169
174
  }
170
175
  function readSingleFileInfo(filePath) {
171
176
  if (!existsSync(filePath)) {
@@ -28,6 +28,7 @@ const PROVIDERS = [
28
28
  "google-html",
29
29
  "exa-mcp",
30
30
  "searxng",
31
+ "keenable",
31
32
  ];
32
33
  /** Engines that need no key or account; a block on any of them puts it on a per-session cooldown. */
33
34
  export const KEYLESS_PROVIDERS = new Set([
@@ -38,6 +39,7 @@ export const KEYLESS_PROVIDERS = new Set([
38
39
  "google-html",
39
40
  "exa-mcp",
40
41
  "searxng",
42
+ "keenable",
41
43
  ]);
42
44
  // These entries may omit apiKey: the search then uses the matching senpi login.
43
45
  const SESSION_LOGIN_PROVIDERS = ["chatgpt-subscription", "google"];
@@ -22,6 +22,7 @@ const DEFAULT_PROVIDER_URLS = {
22
22
  ecosia: "https://www.ecosia.org/search",
23
23
  "google-html": "https://www.google.com/search",
24
24
  "exa-mcp": "https://mcp.exa.ai/mcp",
25
+ keenable: "https://api.keenable.ai/v1/search",
25
26
  };
26
27
  /** SearXNG is self-hosted, so it has no default endpoint; config validation requires its `baseUrl`. */
27
28
  export function defaultProviderUrl(provider) {
@@ -0,0 +1,3 @@
1
+ import type { ProviderModule } from "./shared.ts";
2
+ export declare const keenableProvider: ProviderModule;
3
+ //# sourceMappingURL=keenable.d.ts.map
@@ -0,0 +1,55 @@
1
+ import { APP_NAME } from "../../../../../../config.js";
2
+ import { providerUrl } from "../provider-endpoints.js";
3
+ import { appendDomainFilters, clamp, collect, contentHeaders, getArray, getObject, getString, result, } from "./shared.js";
4
+ /** Keyless twin of the default endpoint; identified by `X-Keenable-Title` instead of a key. */
5
+ const PUBLIC_SEARCH_URL = "https://api.keenable.ai/v1/search/public";
6
+ /** Accepts only absolute http(s) URLs; control characters would let text merge into the URL on parse. */
7
+ function httpUrl(value) {
8
+ if (!value || value.length > 2048 || /[\t\n\r]/.test(value))
9
+ return undefined;
10
+ try {
11
+ const url = new URL(value);
12
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : undefined;
13
+ }
14
+ catch {
15
+ return undefined;
16
+ }
17
+ }
18
+ export const keenableProvider = {
19
+ buildRequest({ config, request, maxResults, allowedDomains, blockedDomains }) {
20
+ // A lone allowed domain maps to the native host-only `site` field; anything else
21
+ // (multiple domains, exclusions) stays as `site:`/`-site:` terms in the query.
22
+ const site = allowedDomains?.length === 1 ? allowedDomains[0] : undefined;
23
+ const body = {
24
+ query: appendDomainFilters(request.query, site ? undefined : allowedDomains, blockedDomains),
25
+ max_results: clamp(maxResults, 1, 50),
26
+ };
27
+ if (site)
28
+ body.site = site;
29
+ const keyed = typeof config.apiKey === "string" && config.apiKey.length > 0;
30
+ return {
31
+ url: keyed || config.baseUrl ? providerUrl(config) : PUBLIC_SEARCH_URL,
32
+ init: {
33
+ method: "POST",
34
+ headers: contentHeaders(keyed ? { "X-API-Key": config.apiKey ?? "" } : { "X-Keenable-Title": APP_NAME }),
35
+ },
36
+ body,
37
+ };
38
+ },
39
+ normalizeResponse(data) {
40
+ return collect(getArray(data.results).map((raw) => {
41
+ const item = getObject(raw);
42
+ const url = httpUrl(getString(item?.url));
43
+ if (!item || !url)
44
+ return null;
45
+ const normalized = result(getString(item.title) ?? url, url, getString(item.snippet) ?? getString(item.description));
46
+ if (normalized) {
47
+ const publishedAt = getString(item.published_at);
48
+ if (publishedAt)
49
+ normalized.publishedAt = publishedAt;
50
+ }
51
+ return normalized;
52
+ }));
53
+ },
54
+ };
55
+ //# sourceMappingURL=keenable.js.map
@@ -11,6 +11,7 @@ import { googleProvider } from "./providers/google.js";
11
11
  import { googleCseProvider } from "./providers/google-cse.js";
12
12
  import { googleHtmlProvider } from "./providers/google-html.js";
13
13
  import { kagiProvider } from "./providers/kagi.js";
14
+ import { keenableProvider } from "./providers/keenable.js";
14
15
  import { kimiProvider } from "./providers/kimi.js";
15
16
  import { mojeekProvider } from "./providers/mojeek.js";
16
17
  import { openAiResponsesProvider } from "./providers/openai-responses.js";
@@ -48,6 +49,7 @@ const PROVIDER_MODULES = {
48
49
  "google-html": googleHtmlProvider,
49
50
  "exa-mcp": exaMcpProvider,
50
51
  searxng: searxngProvider,
52
+ keenable: keenableProvider,
51
53
  };
52
54
  function buildContext(config, request) {
53
55
  const { allowedDomains, blockedDomains } = resolveDomainFilters(config, request);
@@ -1,4 +1,4 @@
1
- export type SearchProvider = "exa" | "tavily" | "serpdive" | "brave" | "duckduckgo-html" | "deepseek" | "serper" | "google-cse" | "z-ai" | "openai" | "codex" | "chatgpt-subscription" | "google" | "anthropic" | "perplexity" | "xai" | "kimi" | "kagi" | "startpage" | "mojeek" | "ecosia" | "google-html" | "exa-mcp" | "searxng";
1
+ export type SearchProvider = "exa" | "tavily" | "serpdive" | "brave" | "duckduckgo-html" | "deepseek" | "serper" | "google-cse" | "z-ai" | "openai" | "codex" | "chatgpt-subscription" | "google" | "anthropic" | "perplexity" | "xai" | "kimi" | "kagi" | "startpage" | "mojeek" | "ecosia" | "google-html" | "exa-mcp" | "searxng" | "keenable";
2
2
  /** Why a keyless engine refused a search; each reason puts the engine on a cooldown. */
3
3
  export type SearchBlockReason = "challenge" | "rate_limited" | "forbidden" | "network";
4
4
  export type SearchContextSize = "low" | "medium" | "high";
@@ -454,6 +454,7 @@ declare const ProviderConfigSchema: Type.TObject<{
454
454
  authHeader: Type.TOptional<Type.TBoolean>;
455
455
  whitelist: Type.TOptional<Type.TArray<Type.TString>>;
456
456
  blacklist: Type.TOptional<Type.TArray<Type.TString>>;
457
+ hideFreeModels: Type.TOptional<Type.TBoolean>;
457
458
  models: Type.TOptional<Type.TArray<Type.TObject<{
458
459
  id: Type.TString;
459
460
  name: Type.TOptional<Type.TString>;
@@ -900,6 +901,7 @@ declare const ModelsConfigSchema: Type.TObject<{
900
901
  authHeader: Type.TOptional<Type.TBoolean>;
901
902
  whitelist: Type.TOptional<Type.TArray<Type.TString>>;
902
903
  blacklist: Type.TOptional<Type.TArray<Type.TString>>;
904
+ hideFreeModels: Type.TOptional<Type.TBoolean>;
903
905
  models: Type.TOptional<Type.TArray<Type.TObject<{
904
906
  id: Type.TString;
905
907
  name: Type.TOptional<Type.TString>;
@@ -1347,6 +1349,7 @@ export declare const validateModelsConfig: import("typebox/compile").Validator<{
1347
1349
  authHeader: Type.TOptional<Type.TBoolean>;
1348
1350
  whitelist: Type.TOptional<Type.TArray<Type.TString>>;
1349
1351
  blacklist: Type.TOptional<Type.TArray<Type.TString>>;
1352
+ hideFreeModels: Type.TOptional<Type.TBoolean>;
1350
1353
  models: Type.TOptional<Type.TArray<Type.TObject<{
1351
1354
  id: Type.TString;
1352
1355
  name: Type.TOptional<Type.TString>;
@@ -1793,6 +1796,7 @@ export declare const validateModelsConfig: import("typebox/compile").Validator<{
1793
1796
  authHeader?: boolean | undefined;
1794
1797
  whitelist?: string[] | undefined;
1795
1798
  blacklist?: string[] | undefined;
1799
+ hideFreeModels?: boolean | undefined;
1796
1800
  models?: {
1797
1801
  id: string;
1798
1802
  name?: string | undefined;
@@ -2239,6 +2243,7 @@ export declare const validateModelsConfig: import("typebox/compile").Validator<{
2239
2243
  authHeader?: boolean | undefined;
2240
2244
  whitelist?: string[] | undefined;
2241
2245
  blacklist?: string[] | undefined;
2246
+ hideFreeModels?: boolean | undefined;
2242
2247
  models?: {
2243
2248
  id: string;
2244
2249
  name?: string | undefined;
@@ -232,6 +232,7 @@ const ProviderConfigSchema = Type.Object({
232
232
  authHeader: Type.Optional(Type.Boolean()),
233
233
  whitelist: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
234
234
  blacklist: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
235
+ hideFreeModels: Type.Optional(Type.Boolean()),
235
236
  models: Type.Optional(Type.Array(ModelDefinitionSchema)),
236
237
  modelOverrides: Type.Optional(Type.Record(Type.String(), ModelOverrideSchema)),
237
238
  credentials: Type.Optional(CredentialPolicySchema),
@@ -165,6 +165,7 @@ function applyModelsJson(providerId, baseModels, config, extension) {
165
165
  !hasOverrides &&
166
166
  !config.whitelist &&
167
167
  !config.blacklist &&
168
+ config.hideFreeModels === undefined &&
168
169
  !config.apiKey &&
169
170
  config.authHeader === undefined) {
170
171
  throw new Error(`Provider ${providerId}: must specify "baseUrl", "headers", "extraBody", "compat", "modelOverrides", or "models".`);
@@ -197,7 +198,10 @@ function applyModelsJson(providerId, baseModels, config, extension) {
197
198
  }
198
199
  const whitelist = config.whitelist ? new Set(config.whitelist) : undefined;
199
200
  const blacklist = config.blacklist ? new Set(config.blacklist) : undefined;
200
- return models.filter((model) => (whitelist === undefined || whitelist.has(model.id)) && (blacklist === undefined || !blacklist.has(model.id)));
201
+ const hideFree = config.hideFreeModels === true;
202
+ return models.filter((model) => (whitelist === undefined || whitelist.has(model.id)) &&
203
+ (blacklist === undefined || !blacklist.has(model.id)) &&
204
+ !(hideFree && model.cost.input === 0 && model.cost.output === 0));
201
205
  }
202
206
  function applyExtension(providerId, models, config, customModelIds) {
203
207
  if (!config)
@@ -15,7 +15,13 @@ export interface BoundedRetryContinuation {
15
15
  getActiveSignal(): AbortSignal | undefined;
16
16
  abortActive(): void;
17
17
  timeoutMs: number | undefined;
18
+ /**
19
+ * Subscribes to the retried request's first stream event (its assistant `message_start`); returns
20
+ * the unsubscribe. Once the request streams, the provider's own idle and stall guards govern the rest
21
+ * of the run, so the watchdog is cleared (senpi#2804).
22
+ */
23
+ onStreamStarted?(listener: () => void): () => void;
18
24
  }
19
25
  export declare function createProviderTimeoutRetryPlan({ message, streamRetryTimeoutMs, timeoutMs, streamStartTimeoutMs, }: ProviderTimeoutRetryPlanInput): ProviderTimeoutRetryPlan;
20
- export declare function runBoundedRetryContinuation({ continueRun, getActiveSignal, abortActive, timeoutMs, }: BoundedRetryContinuation): Promise<void>;
26
+ export declare function runBoundedRetryContinuation({ continueRun, getActiveSignal, abortActive, timeoutMs, onStreamStarted, }: BoundedRetryContinuation): Promise<void>;
21
27
  //# sourceMappingURL=provider-timeout-retry.d.ts.map
@@ -42,23 +42,34 @@ export function createProviderTimeoutRetryPlan({ message, streamRetryTimeoutMs,
42
42
  watchdogTimeoutMs: reconcileWatchdogTimeoutMs(streamRetryTimeoutMs, streamStartTimeoutMs),
43
43
  };
44
44
  }
45
- export async function runBoundedRetryContinuation({ continueRun, getActiveSignal, abortActive, timeoutMs, }) {
46
- const continuation = continueRun();
47
- const ownedSignal = getActiveSignal();
48
- if (timeoutMs === undefined || ownedSignal === undefined) {
49
- await continuation;
50
- return;
51
- }
52
- const timer = setTimeout(() => {
53
- if (getActiveSignal() === ownedSignal) {
54
- abortActive();
55
- }
56
- }, timeoutMs);
45
+ export async function runBoundedRetryContinuation({ continueRun, getActiveSignal, abortActive, timeoutMs, onStreamStarted, }) {
46
+ // The watchdog bounds only the wait for the retried request to start streaming: a retry that streams
47
+ // and then works for a long time (a long answer, tool calls, further turns) is not a wedged retry.
48
+ let streamStarted = false;
49
+ let timer;
50
+ const unsubscribe = timeoutMs === undefined
51
+ ? undefined
52
+ : onStreamStarted?.(() => {
53
+ streamStarted = true;
54
+ clearTimeout(timer);
55
+ });
57
56
  try {
57
+ const continuation = continueRun();
58
+ const ownedSignal = getActiveSignal();
59
+ if (timeoutMs === undefined || ownedSignal === undefined || streamStarted) {
60
+ await continuation;
61
+ return;
62
+ }
63
+ timer = setTimeout(() => {
64
+ if (!streamStarted && getActiveSignal() === ownedSignal) {
65
+ abortActive();
66
+ }
67
+ }, timeoutMs);
58
68
  await continuation;
59
69
  }
60
70
  finally {
61
71
  clearTimeout(timer);
72
+ unsubscribe?.();
62
73
  }
63
74
  }
64
75
  //# sourceMappingURL=provider-timeout-retry.js.map
@@ -1,4 +1,4 @@
1
1
  import type { ExtensionRunner } from "./extensions/runner.ts";
2
2
  import type { ReloadVetoDecision } from "./extensions/types.ts";
3
- export declare function checkSessionReloadVeto(runner: Pick<ExtensionRunner, "hasHandlers" | "emit">, isPromptStartPending: () => boolean): Promise<ReloadVetoDecision>;
3
+ export declare function checkSessionReloadVeto(runner: Pick<ExtensionRunner, "hasHandlers" | "emit">, isPromptStartPending: () => boolean, isSessionStartDispatching?: () => boolean): Promise<ReloadVetoDecision>;
4
4
  //# sourceMappingURL=reload-veto.d.ts.map
@@ -1,13 +1,19 @@
1
- export async function checkSessionReloadVeto(runner, isPromptStartPending) {
1
+ export async function checkSessionReloadVeto(runner, isPromptStartPending, isSessionStartDispatching = () => false) {
2
2
  if (isPromptStartPending()) {
3
3
  return { cancelled: true, reason: "A prompt is being admitted." };
4
4
  }
5
+ if (isSessionStartDispatching()) {
6
+ return { cancelled: true, reason: "A session is starting." };
7
+ }
5
8
  const result = runner.hasHandlers("session_before_reload")
6
9
  ? await runner.emit({ type: "session_before_reload" })
7
10
  : undefined;
8
11
  if (isPromptStartPending()) {
9
12
  return { cancelled: true, reason: "A prompt is being admitted." };
10
13
  }
14
+ if (isSessionStartDispatching()) {
15
+ return { cancelled: true, reason: "A session is starting." };
16
+ }
11
17
  if (result?.cancel !== true)
12
18
  return { cancelled: false };
13
19
  return result.reason === undefined ? { cancelled: true } : { cancelled: true, reason: result.reason };
@@ -1,11 +1,17 @@
1
1
  import { isValidThinkingLevel } from "../../cli/args.js";
2
2
  /**
3
- * Providers that never receive a bare selector. OpenRouter re-publishes other
3
+ * Providers that never receive a bare selector. Gateways re-publish other
4
4
  * vendors' catalogs under namespaced ids, so family expansion would silently
5
5
  * route a shipped default through a third-party reseller the user never chose.
6
- * An explicit `openrouter/...` selector the user wrote is unaffected.
6
+ * An explicit provider-qualified selector the user wrote is unaffected.
7
7
  */
8
- const BARE_EXPANSION_DENYLIST = new Set(["cursor", "openrouter", "openrouter-images"]);
8
+ const BARE_EXPANSION_DENYLIST = new Set([
9
+ "cursor",
10
+ "openrouter",
11
+ "openrouter-images",
12
+ "opengateway",
13
+ "vercel-ai-gateway",
14
+ ]);
9
15
  /**
10
16
  * Deterministic tie-break inside one auth tier. Earlier wins. Providers absent
11
17
  * from the table sort after every listed one, alphabetically.
package/dist/core/sdk.js CHANGED
@@ -20,7 +20,7 @@ import { mergeProviderAttributionHeaders } from "./provider-attribution.js";
20
20
  import { DefaultResourceLoader } from "./resource-loader.js";
21
21
  import { getDefaultSessionDir, SessionManager } from "./session-manager.js";
22
22
  import { DEFAULT_TOOL_NAMES, SettingsManager } from "./settings-manager.js";
23
- import { getSupportedThinkingLevels } from "./thinking-levels.js";
23
+ import { clampThinkingSelection, getSupportedThinkingLevels } from "./thinking-levels.js";
24
24
  import { time } from "./timings.js";
25
25
  import { createBashTool, createCodingTools, createEditTool, createFindTool, createGrepTool, createLsTool, createPowerShellTool, createReadOnlyTools, createReadTool, createWriteTool, withFileMutationQueue, } from "./tools/index.js";
26
26
  import { serviceTierForProvider } from "./ultrafast-lanes.js";
@@ -201,25 +201,29 @@ export async function createAgentSession(options = {}) {
201
201
  if (thinkingLevel === undefined && model?.defaultThinkingLevel !== undefined) {
202
202
  thinkingLevel = model.defaultThinkingLevel;
203
203
  }
204
+ let thinkingFromGlobalDefault = false;
204
205
  if (thinkingLevel === undefined) {
205
206
  const configuredDefault = settingsManager.getDefaultThinkingLevel();
206
207
  if (configuredDefault !== undefined) {
207
208
  thinkingLevel = configuredDefault;
208
209
  thinkingSelection = { level: configuredDefault, source: "explicit" };
210
+ thinkingFromGlobalDefault = true;
209
211
  }
210
212
  else {
211
213
  thinkingLevel = DEFAULT_THINKING_LEVEL;
212
214
  }
213
215
  }
214
216
  // Clamp to model capabilities without inventing provenance for a defaulted level.
217
+ const requestedThinkingLevel = thinkingLevel;
215
218
  if (!model) {
216
219
  thinkingLevel = "off";
217
220
  }
218
221
  else {
219
222
  thinkingLevel = clampThinkingLevelToModel(thinkingLevel, model);
220
223
  }
221
- if (thinkingSelection)
222
- thinkingSelection = { ...thinkingSelection, level: thinkingLevel };
224
+ // senpi#2395: an explicit request the clamp changed keeps the requested level and the reason. The global
225
+ // default is a default, not a request, so its clamp records no requested level and shows no warning.
226
+ thinkingSelection = clampThinkingSelection(thinkingSelection, thinkingFromGlobalDefault ? thinkingLevel : requestedThinkingLevel, thinkingLevel, model);
223
227
  const configuredDefaultToolNames = settingsManager.getDefaultTools();
224
228
  const sessionDefaultToolNames = options.tools === undefined && options.noTools === undefined ? configuredDefaultToolNames : undefined;
225
229
  const allowedToolNames = options.tools ?? (options.noTools === "all" ? [] : undefined);
@@ -1,5 +1,5 @@
1
1
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
- import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import type { Api, Model, ModelThinkingLevel, ThinkingSelection } from "@earendil-works/pi-ai";
3
3
  /**
4
4
  * Tier detection is owned by `packages/ai`, which already treats an explicit `thinkingLevelMap` as
5
5
  * authoritative and infers tiers from the model id only when no map is present. These wrappers keep
@@ -8,6 +8,28 @@ import type { Api, Model } from "@earendil-works/pi-ai";
8
8
  export declare function supportsXhigh(model: Model<Api>): boolean;
9
9
  export declare function supportsMax(model: Model<Api>): boolean;
10
10
  export declare function getSupportedThinkingLevels(model: Model<Api>): ThinkingLevel[];
11
+ /** Why an explicit thinking selection runs at a lower level than the one requested (senpi#2395). */
12
+ export type ThinkingClampReason = "model-not-reasoning" | "level-unsupported";
13
+ /** An explicit selection that also records the requested level when a clamp changed it. */
14
+ export interface ClampedThinkingSelection extends ThinkingSelection {
15
+ readonly requested?: ModelThinkingLevel;
16
+ readonly clampReason?: ThinkingClampReason;
17
+ }
18
+ /** One clamp of an explicit request, as shown to the user. */
19
+ export interface ThinkingClampNotice {
20
+ provider: string;
21
+ modelId: string;
22
+ requestedLevel: ModelThinkingLevel;
23
+ appliedLevel: ModelThinkingLevel;
24
+ reason: ThinkingClampReason;
25
+ }
26
+ /**
27
+ * Apply the effective `level` to an explicit selection. When it differs from `requested`, the selection keeps
28
+ * the requested level and the clamp reason instead of reading as if `level` had been chosen.
29
+ */
30
+ export declare function clampThinkingSelection(selection: ThinkingSelection | undefined, requested: ModelThinkingLevel, level: ModelThinkingLevel, model: Model<Api> | undefined): ThinkingSelection | undefined;
31
+ export declare function getThinkingClampNotice(selection: ThinkingSelection | undefined, model: Model<Api> | undefined): ThinkingClampNotice | undefined;
32
+ export declare function formatThinkingClampWarning(notice: ThinkingClampNotice): string;
11
33
  /** Capability classes used by capability-aware reasoning commands. */
12
34
  export type ReasoningCapabilityKind = "none" | "always-on" | "on-off" | "graded";
13
35
  export interface ReasoningCapability {