@selesai/code 0.13.18 → 0.13.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/defaults/models.json +22 -3
- package/dist/extensions/agent-browser.ts +1 -2
- package/dist/extensions/capability-gateway/catalog.ts +26 -2
- package/dist/extensions/capability-gateway/index.test.ts +9 -0
- package/dist/extensions/package.json +0 -1
- package/dist/extensions/pi-web-agent/README.md +6 -5
- package/dist/extensions/pi-web-agent/src/backends/config.ts +12 -6
- package/dist/extensions/pi-web-agent/src/backends/factory.ts +2 -2
- package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +4 -5
- package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +8 -8
- package/dist/extensions/pi-web-agent/src/extension.ts +3 -3
- package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +2 -2
- package/dist/extensions/pi-web-agent/src/search/tokenin.ts +14 -3
- package/package.json +3 -3
- package/dist/extensions/web-agent-onboarding.test.ts +0 -415
- package/dist/extensions/web-agent-onboarding.ts +0 -222
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@selesai/code` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.13.20] - 2026-09-10
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **DeepSeek V4.1 Flash in default catalog.** Added `deepseek-v4.1-flash` (text+image, 512K context, 128K max tokens, deepseek thinking format).
|
|
9
|
+
- **Vision for DeepSeek V4 Pro.** `deepseek-v4-pro` now accepts text+image input with 128K max tokens.
|
|
10
|
+
- **Typo-tolerant capability routing.** The capability gateway now scores one-character-off name/alias matches as exact, so a misspelled tool name (e.g. `inercom` → `intercom`) auto-activates when uniquely identifiable.
|
|
11
|
+
|
|
12
|
+
## [0.13.19] - 2026-09-09
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **Conditional web-search default.** pi-web-agent now defaults search to TokenIn when an active TokenIn account exists, and to keyless DuckDuckGo otherwise — no more TokenIn requests without an account.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **TokenIn search timeout.** TokenIn search requests abort after 10s and fall back to DuckDuckGo instead of hanging.
|
|
19
|
+
|
|
20
|
+
### Removed
|
|
21
|
+
- **Brave web-agent onboarding.** The `web-agent-onboarding` extension, its `/setup-web-search` flow, and its docs are removed; existing manually configured Brave keys in `settings.json` are still honored.
|
|
22
|
+
|
|
5
23
|
## [0.13.18] - 2026-09-07
|
|
6
24
|
|
|
7
25
|
### Removed
|
|
@@ -105,16 +105,35 @@
|
|
|
105
105
|
"requiresReasoningContentOnAssistantMessages": true
|
|
106
106
|
}
|
|
107
107
|
},
|
|
108
|
+
{
|
|
109
|
+
"id": "deepseek-v4.1-flash",
|
|
110
|
+
"name": "DeepSeek V4.1 Flash",
|
|
111
|
+
"reasoning": true,
|
|
112
|
+
"input": ["text", "image"],
|
|
113
|
+
"contextWindow": 512000,
|
|
114
|
+
"maxTokens": 128000,
|
|
115
|
+
"thinkingLevelMap": {
|
|
116
|
+
"minimal": null,
|
|
117
|
+
"low": null,
|
|
118
|
+
"medium": null,
|
|
119
|
+
"high": "high",
|
|
120
|
+
"xhigh": null,
|
|
121
|
+
"max": "max"
|
|
122
|
+
},
|
|
123
|
+
"compat": {
|
|
124
|
+
"requiresReasoningContentOnAssistantMessages": true
|
|
125
|
+
}
|
|
126
|
+
},
|
|
108
127
|
{
|
|
109
128
|
"id": "deepseek-v4-pro",
|
|
110
129
|
"name": "DeepSeek V4 pro",
|
|
111
130
|
"reasoning": true,
|
|
112
|
-
"input": ["text"],
|
|
131
|
+
"input": ["text", "image"],
|
|
113
132
|
"contextWindow": 512000,
|
|
114
|
-
"maxTokens":
|
|
133
|
+
"maxTokens": 128000,
|
|
115
134
|
"thinkingLevelMap": {
|
|
116
135
|
"minimal": null,
|
|
117
|
-
"low":
|
|
136
|
+
"low": null,
|
|
118
137
|
"medium": null,
|
|
119
138
|
"high": "high",
|
|
120
139
|
"xhigh": null,
|
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
* - With no UI (headless/CI), fall back to a console warning with the install
|
|
13
13
|
* command, mirroring the rtk extension's fail-open behavior.
|
|
14
14
|
*
|
|
15
|
-
* Mirrors src/extensions/rtk.ts (probe, opt-out env var, fail-open warning)
|
|
16
|
-
* and src/extensions/web-agent-onboarding.ts (confirm -> act -> marker).
|
|
15
|
+
* Mirrors src/extensions/rtk.ts (probe, opt-out env var, fail-open warning).
|
|
17
16
|
*/
|
|
18
17
|
|
|
19
18
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
@@ -86,6 +86,29 @@ export function tokenize(text: string): string[] {
|
|
|
86
86
|
return normalizeQuery(text).split(/\s+/).filter(Boolean);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/** True when two words differ by at most one insertion, deletion, or substitution. */
|
|
90
|
+
function differsByOne(left: string, right: string): boolean {
|
|
91
|
+
if (Math.abs(left.length - right.length) > 1) return false;
|
|
92
|
+
let leftIndex = 0;
|
|
93
|
+
let rightIndex = 0;
|
|
94
|
+
let differences = 0;
|
|
95
|
+
while (leftIndex < left.length && rightIndex < right.length) {
|
|
96
|
+
if (left[leftIndex] === right[rightIndex]) {
|
|
97
|
+
leftIndex++;
|
|
98
|
+
rightIndex++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (++differences > 1) return false;
|
|
102
|
+
if (left.length > right.length) leftIndex++;
|
|
103
|
+
else if (right.length > left.length) rightIndex++;
|
|
104
|
+
else {
|
|
105
|
+
leftIndex++;
|
|
106
|
+
rightIndex++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
89
112
|
/** Words that carry no routing signal. */
|
|
90
113
|
const STOPWORDS = new Set([
|
|
91
114
|
"the", "a", "an", "to", "of", "for", "with", "and", "or", "use", "using", "me", "my", "i",
|
|
@@ -103,8 +126,8 @@ export interface RouteResult {
|
|
|
103
126
|
/**
|
|
104
127
|
* Deterministic routing over the compact catalog.
|
|
105
128
|
*
|
|
106
|
-
* - Exact name/alias
|
|
107
|
-
* shared with the name/alias or summary adds 1.
|
|
129
|
+
* - Exact or one-character-off name/alias matches score 3; prefix matches score 2;
|
|
130
|
+
* each content token shared with the name/alias or summary adds 1.
|
|
108
131
|
* - A unique tool with score >= 3 is auto-activated.
|
|
109
132
|
* - A unique skill with score >= 3 is recommended (never auto-loaded).
|
|
110
133
|
* - Score 2 or a tie at the top produces a catalog-discovery hint.
|
|
@@ -127,6 +150,7 @@ export function route(query: string, catalog: CatalogEntry[]): RouteResult {
|
|
|
127
150
|
}
|
|
128
151
|
for (const token of contentTokens) {
|
|
129
152
|
if (nameTokens.has(token)) score += 3;
|
|
153
|
+
else if (token.length >= 5 && [...nameTokens].some((nameToken) => nameToken.length >= 5 && differsByOne(token, nameToken))) score += 3;
|
|
130
154
|
else if (token.length >= 3 && [...nameTokens].some((nameToken) => nameToken.startsWith(token))) score += 2;
|
|
131
155
|
if (summaryTokens.has(token)) score += 1;
|
|
132
156
|
}
|
|
@@ -102,6 +102,15 @@ describe("deterministic routing", () => {
|
|
|
102
102
|
expect(result.entry?.name).toBe("grep_app_search");
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
+
it("auto-activates a uniquely identifiable one-character tool typo", () => {
|
|
106
|
+
const result = route("activate inercom", [
|
|
107
|
+
...catalog,
|
|
108
|
+
{ name: "intercom", kind: "tool", summary: "Coordinate with other local sessions", aliases: [], category: "coordination" },
|
|
109
|
+
]);
|
|
110
|
+
expect(result.action).toBe("activate");
|
|
111
|
+
expect(result.entry?.name).toBe("intercom");
|
|
112
|
+
});
|
|
113
|
+
|
|
105
114
|
it("recommends a unique high-confidence skill without auto-loading", () => {
|
|
106
115
|
const result = route("do research on this topic", catalog);
|
|
107
116
|
expect(result.action).toBe("recommend");
|
|
@@ -16,18 +16,18 @@ One public tool, `web_explore`, that does bounded web research for Pi: search, f
|
|
|
16
16
|
|
|
17
17
|
- **One tool.** `web_explore` handles direct links, discovery, HTTP reads, targeted headless rendering, source ranking, source-quality checks, and caveats internally.
|
|
18
18
|
- **Reads the real content behind links.** Paste a GitHub, PDF, or YouTube URL and it pulls the actual thing (GitHub files/issues/PRs from the API, PDF text, YouTube transcripts), keyless. So "summarize this PDF" or "what does this repo do" works off the source, not the page shell.
|
|
19
|
-
- **
|
|
19
|
+
- **Seven search backends.** TokenIn (default with an active account), DuckDuckGo (keyless otherwise), SearXNG, Brave, You.com, Exa, Tavily.
|
|
20
20
|
- **Optional search fanout.** Query several backends at once, dedupe, and rank pages that more than one provider agreed on to the top. Off by default; flip it to `on` or `auto`.
|
|
21
21
|
- **Honest by default.** Weak, narrow, blocked, or cautionary evidence gets flagged instead of dressed up as confidence.
|
|
22
22
|
- **Bounded output.** `compact` / `preview` / `verbose` transcript modes.
|
|
23
|
-
- **Zero-config to start.**
|
|
23
|
+
- **Zero-config to start.** Without an active TokenIn account, runs keyless out of the box (DuckDuckGo search, local browser, the built-in readers). Opt into hosted backends, fallback, search fanout, and per-tool output modes through config when you want more control.
|
|
24
24
|
|
|
25
25
|
## Why pi-web-agent
|
|
26
26
|
|
|
27
27
|
Compared to other web tooling for agents:
|
|
28
28
|
|
|
29
29
|
- **Hands-off.** No curator, no browser windows to approve, no step that pops you out of your session. Ask `web_explore` once and the answer comes back with caveats. Nothing to babysit.
|
|
30
|
-
- **Keyless
|
|
30
|
+
- **Keyless without TokenIn.** Search, page reads, and the GitHub/PDF/YouTube readers all work with no API keys when TokenIn is not configured. Add hosted providers only when you want them.
|
|
31
31
|
- **Bounded and honest.** Compact output by default, and it says when a read was not good enough instead of returning fake confidence.
|
|
32
32
|
|
|
33
33
|
## Install
|
|
@@ -63,11 +63,12 @@ If a pass comes back thin, call `web_explore` again with a narrower query.
|
|
|
63
63
|
|
|
64
64
|
## Backends
|
|
65
65
|
|
|
66
|
-
Defaults are
|
|
66
|
+
Defaults are TokenIn search when an active TokenIn account exists, otherwise DuckDuckGo search; fetch remains plain HTTP and headless remains local-browser. Switch providers from `/web-agent settings → Backends`. API keys stay in environment variables, never in config files.
|
|
67
67
|
|
|
68
68
|
| Backend | Role | Enable with |
|
|
69
69
|
| --- | --- | --- |
|
|
70
|
-
|
|
|
70
|
+
| TokenIn | search (default with an active account) | `/tokenin add` |
|
|
71
|
+
| DuckDuckGo | search (default without TokenIn) | nothing, keyless |
|
|
71
72
|
| SearXNG | search (self-hosted) | base URL |
|
|
72
73
|
| Brave | search (hosted) | `PI_WEB_AGENT_BRAVE_API_KEY` |
|
|
73
74
|
| You.com | search (hosted) | `YDC_API_KEY` |
|
|
@@ -54,11 +54,17 @@ export type BackendConfigFile = {
|
|
|
54
54
|
};
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
-
export
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
export function getDefaultBackendConfig(
|
|
58
|
+
hasActiveAccount: () => boolean = hasActiveTokenInAccount
|
|
59
|
+
): BackendConfig {
|
|
60
|
+
return {
|
|
61
|
+
search: hasActiveAccount()
|
|
62
|
+
? { provider: 'tokenin', fallback: 'duckduckgo' }
|
|
63
|
+
: { provider: 'duckduckgo' },
|
|
64
|
+
fetch: { provider: 'http' },
|
|
65
|
+
headless: { provider: 'local-browser' }
|
|
66
|
+
};
|
|
67
|
+
}
|
|
62
68
|
|
|
63
69
|
function extractStringArray(value: unknown): string[] | undefined {
|
|
64
70
|
if (!Array.isArray(value)) return undefined;
|
|
@@ -260,6 +266,6 @@ export function mergeBackendConfigLayers(
|
|
|
260
266
|
fetch: mergeFetchConfig(merged.fetch, layer?.fetch),
|
|
261
267
|
headless: { ...merged.headless, ...layer?.headless }
|
|
262
268
|
}),
|
|
263
|
-
|
|
269
|
+
getDefaultBackendConfig()
|
|
264
270
|
);
|
|
265
271
|
}
|
|
@@ -13,7 +13,7 @@ import { createWebFetchTool } from '../tools/web-fetch.js';
|
|
|
13
13
|
import { createWebSearchTool } from '../tools/web-search.js';
|
|
14
14
|
import { readBraveKeyFromSettings } from './settings-reader.js';
|
|
15
15
|
import type { SearchProviderName, WebFetchHeadlessResponse, WebFetchResponse, WebSearchResponse } from '../types.js';
|
|
16
|
-
import {
|
|
16
|
+
import { getDefaultBackendConfig, type BackendConfig, usableSearchProviders } from './config.js';
|
|
17
17
|
import { createSpecialContentResolver } from '../readers/resolver.js';
|
|
18
18
|
import { createGithubReader } from '../readers/github-reader.js';
|
|
19
19
|
import { createPdfReader } from '../readers/pdf-reader.js';
|
|
@@ -117,7 +117,7 @@ function withFetchFallback(
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
export function createBackendSet(
|
|
120
|
-
config: BackendConfig =
|
|
120
|
+
config: BackendConfig = getDefaultBackendConfig(),
|
|
121
121
|
deps: BackendFactoryDeps = {}
|
|
122
122
|
): BackendSet {
|
|
123
123
|
const createDuckDuckGoSearch = deps.createDuckDuckGoSearch ?? createWebSearchTool;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
// ponytail:
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// helper needs updating; upgrade path = upstream pi-web-agent adopting a settings hook.
|
|
1
|
+
// ponytail: preserve existing manually configured Brave keys in settings.json
|
|
2
|
+
// (webAgent.braveApiKey). Upstream pi-web-agent reads only PI_WEB_AGENT_BRAVE_API_KEY.
|
|
3
|
+
// Ceiling: if selesai moves web search config elsewhere, update this helper; upgrade path =
|
|
4
|
+
// upstream pi-web-agent adopting a settings hook.
|
|
6
5
|
import { existsSync, readFileSync } from 'node:fs';
|
|
7
6
|
import { join } from 'node:path';
|
|
8
7
|
import { getAgentDir, getSettingsPath } from '@selesai/code';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
getDefaultBackendConfig,
|
|
3
3
|
mergeBackendConfigLayers,
|
|
4
4
|
validateBackendConfig,
|
|
5
5
|
usableSearchProviders,
|
|
@@ -150,7 +150,7 @@ function formatFetchOptions(config: BackendConfig['fetch']) {
|
|
|
150
150
|
].filter(Boolean).join(' ');
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
function formatBackendSummary(config: BackendConfig =
|
|
153
|
+
function formatBackendSummary(config: BackendConfig = getDefaultBackendConfig()) {
|
|
154
154
|
const searchSuffix = formatSearchOptions(config.search);
|
|
155
155
|
const fetchSuffix = formatFetchOptions(config.fetch);
|
|
156
156
|
const searchBase = config.search.baseUrl
|
|
@@ -394,10 +394,10 @@ export function getInheritedBackendsForScope(
|
|
|
394
394
|
scope: PresentationScope
|
|
395
395
|
): BackendConfig {
|
|
396
396
|
if (scope === 'global') {
|
|
397
|
-
return
|
|
397
|
+
return getDefaultBackendConfig();
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
-
return mergeBackendConfigLayers(
|
|
400
|
+
return mergeBackendConfigLayers(getDefaultBackendConfig(), loaded.global.rawBackends);
|
|
401
401
|
}
|
|
402
402
|
|
|
403
403
|
export function getScopeDisplayBackends(
|
|
@@ -405,11 +405,11 @@ export function getScopeDisplayBackends(
|
|
|
405
405
|
scope: PresentationScope
|
|
406
406
|
): BackendConfig {
|
|
407
407
|
if (scope === 'global') {
|
|
408
|
-
return mergeBackendConfigLayers(
|
|
408
|
+
return mergeBackendConfigLayers(getDefaultBackendConfig(), loaded.global.rawBackends);
|
|
409
409
|
}
|
|
410
410
|
|
|
411
411
|
return mergeBackendConfigLayers(
|
|
412
|
-
|
|
412
|
+
getDefaultBackendConfig(),
|
|
413
413
|
loaded.global.rawBackends,
|
|
414
414
|
loaded.project.rawBackends
|
|
415
415
|
);
|
|
@@ -942,7 +942,7 @@ export function registerWebAgentConfigCommands(pi: ExtensionAPI, deps: CommandDe
|
|
|
942
942
|
|
|
943
943
|
if (action === 'doctor') {
|
|
944
944
|
const [typeboxOk, browser, loaded] = await Promise.all([checkTypebox(), resolveBrowser(), load()]);
|
|
945
|
-
const backendConfig = loaded.effectiveBackends ??
|
|
945
|
+
const backendConfig = loaded.effectiveBackends ?? getDefaultBackendConfig();
|
|
946
946
|
const backendIssues = validateBackendConfig(backendConfig);
|
|
947
947
|
const backendHealth = await checkBackends(backendConfig);
|
|
948
948
|
const lines = [
|
|
@@ -971,7 +971,7 @@ export function registerWebAgentConfigCommands(pi: ExtensionAPI, deps: CommandDe
|
|
|
971
971
|
ctx.ui.notify(
|
|
972
972
|
[
|
|
973
973
|
formatConfigSummary(loaded.effectiveConfig),
|
|
974
|
-
formatBackendSummary(loaded.effectiveBackends ??
|
|
974
|
+
formatBackendSummary(loaded.effectiveBackends ?? getDefaultBackendConfig()),
|
|
975
975
|
`global: ${loaded.global.path}${loaded.global.exists ? '' : ' (missing)'}`,
|
|
976
976
|
`project: ${loaded.project.path}${loaded.project.exists ? '' : ' (missing)'}`
|
|
977
977
|
].join('\n'),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from '@selesai/code';
|
|
2
|
-
import {
|
|
2
|
+
import { getDefaultBackendConfig, type BackendConfig } from './backends/config.js';
|
|
3
3
|
import { Type } from 'typebox';
|
|
4
4
|
import { registerWebAgentConfigCommands } from './commands/web-agent-config.js';
|
|
5
5
|
import { DEFAULT_PRESENTATION_CONFIG, resolvePresentationMode } from './presentation/config.js';
|
|
@@ -60,9 +60,9 @@ async function getEffectivePresentationConfig(pi: ExtensionAPI): Promise<Present
|
|
|
60
60
|
async function getEffectiveBackendConfig(pi: ExtensionAPI): Promise<BackendConfig> {
|
|
61
61
|
try {
|
|
62
62
|
const loaded = await loadWebAgentConfig(pi);
|
|
63
|
-
return loaded.effectiveBackends ??
|
|
63
|
+
return loaded.effectiveBackends ?? getDefaultBackendConfig();
|
|
64
64
|
} catch {
|
|
65
|
-
return
|
|
65
|
+
return getDefaultBackendConfig();
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -2,7 +2,7 @@ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { CONFIG_DIR_NAME, getAgentDir } from '@selesai/code';
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
getDefaultBackendConfig,
|
|
6
6
|
extractBackendConfigOverride,
|
|
7
7
|
mergeBackendConfigLayers,
|
|
8
8
|
type BackendConfig,
|
|
@@ -162,7 +162,7 @@ export async function loadPresentationConfigLayers(
|
|
|
162
162
|
project.rawConfig
|
|
163
163
|
),
|
|
164
164
|
effectiveBackends: mergeBackendConfigLayers(
|
|
165
|
-
|
|
165
|
+
getDefaultBackendConfig(),
|
|
166
166
|
global.rawBackends,
|
|
167
167
|
project.rawBackends
|
|
168
168
|
)
|
|
@@ -6,6 +6,7 @@ import type { SearchResult, WebSearchResponse } from '../types.js';
|
|
|
6
6
|
|
|
7
7
|
export const TOKENIN_SEARCH_TOOL_NAME = 'firecrawl';
|
|
8
8
|
export const TOKENIN_DEFAULT_BASE_URL = 'https://lite.andlet.me/v1';
|
|
9
|
+
const TOKENIN_SEARCH_TIMEOUT_MS = 10_000;
|
|
9
10
|
|
|
10
11
|
type TokenInAccount = { id: string; label: string; apiKey: string; baseUrl?: string };
|
|
11
12
|
type TokenInAuth = { accounts: TokenInAccount[]; activeId: string | null };
|
|
@@ -54,10 +55,12 @@ function normalizeResults(response: LiteLLMSearchResponse): SearchResult[] {
|
|
|
54
55
|
|
|
55
56
|
export function createTokenInSearchTool({
|
|
56
57
|
readAccount = readActiveTokenInAccount,
|
|
57
|
-
fetchImpl = fetch
|
|
58
|
+
fetchImpl = fetch,
|
|
59
|
+
timeoutMs = TOKENIN_SEARCH_TIMEOUT_MS
|
|
58
60
|
}: {
|
|
59
61
|
readAccount?: () => TokenInAccount | undefined;
|
|
60
62
|
fetchImpl?: typeof fetch;
|
|
63
|
+
timeoutMs?: number;
|
|
61
64
|
} = {}) {
|
|
62
65
|
return async function tokenInSearch({ query }: { query: string }): Promise<WebSearchResponse> {
|
|
63
66
|
const normalizedQuery = query.trim();
|
|
@@ -89,6 +92,9 @@ export function createTokenInSearchTool({
|
|
|
89
92
|
.replace(/\/v1$/, '');
|
|
90
93
|
const url = `${rootUrl}/v1/search/${TOKENIN_SEARCH_TOOL_NAME}`;
|
|
91
94
|
|
|
95
|
+
const controller = new AbortController();
|
|
96
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
97
|
+
|
|
92
98
|
try {
|
|
93
99
|
const response = await fetchImpl(url, {
|
|
94
100
|
method: 'POST',
|
|
@@ -97,7 +103,8 @@ export function createTokenInSearchTool({
|
|
|
97
103
|
'Content-Type': 'application/json',
|
|
98
104
|
Authorization: `Bearer ${account.apiKey}`
|
|
99
105
|
},
|
|
100
|
-
body: JSON.stringify({ query: normalizedQuery, max_results: 10 })
|
|
106
|
+
body: JSON.stringify({ query: normalizedQuery, max_results: 10 }),
|
|
107
|
+
signal: controller.signal
|
|
101
108
|
});
|
|
102
109
|
|
|
103
110
|
if (!response.ok) {
|
|
@@ -122,13 +129,17 @@ export function createTokenInSearchTool({
|
|
|
122
129
|
metadata: { backend: 'tokenin', cacheHit: false }
|
|
123
130
|
});
|
|
124
131
|
} catch (error) {
|
|
125
|
-
const rawMessage =
|
|
132
|
+
const rawMessage = controller.signal.aborted
|
|
133
|
+
? `Token-In search timed out after ${timeoutMs}ms.`
|
|
134
|
+
: error instanceof Error ? error.message : String(error);
|
|
126
135
|
return resultWithPresentation({
|
|
127
136
|
status: 'error',
|
|
128
137
|
results: [],
|
|
129
138
|
metadata: { backend: 'tokenin', cacheHit: false },
|
|
130
139
|
error: { code: 'FETCH_FAILED', message: `Token-In search request failed: ${rawMessage}` }
|
|
131
140
|
});
|
|
141
|
+
} finally {
|
|
142
|
+
clearTimeout(timeout);
|
|
132
143
|
}
|
|
133
144
|
};
|
|
134
145
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selesai/code",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.20",
|
|
4
4
|
"description": "Maintained, extension-first Pi coding agent with built-in workflows, subagents, web research, questions, skills, and an enhanced terminal UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"clean": "shx rm -rf dist",
|
|
53
53
|
"dev": "tsx src/cli.ts",
|
|
54
54
|
"dev:print": "tsx src/cli.ts --print",
|
|
55
|
-
"test": "vitest run src/extensions/undo.test.ts src/extensions/tps.test.ts src/extensions/agent-browser.test.ts src/extensions/auto-session-name.test.ts src/extensions/context-compaction-reminder.test.ts src/extensions/copy-turn.test.ts src/extensions/grep-app/index.test.ts src/extensions/inline-skills.test.ts src/extensions/rtk.test.ts src/extensions/
|
|
56
|
-
"test:coverage": "vitest run --coverage src/extensions/undo.test.ts src/extensions/tps.test.ts src/extensions/agent-browser.test.ts src/extensions/auto-session-name.test.ts src/extensions/context-compaction-reminder.test.ts src/extensions/copy-turn.test.ts src/extensions/grep-app/index.test.ts src/extensions/inline-skills.test.ts src/extensions/rtk.test.ts src/extensions/
|
|
55
|
+
"test": "vitest run src/extensions/undo.test.ts src/extensions/tps.test.ts src/extensions/agent-browser.test.ts src/extensions/auto-session-name.test.ts src/extensions/context-compaction-reminder.test.ts src/extensions/copy-turn.test.ts src/extensions/grep-app/index.test.ts src/extensions/inline-skills.test.ts src/extensions/rtk.test.ts src/extensions/handoff-new.test.ts src/extensions/question/tests src/extensions/ponytail/test src/__tests__/tokenin-onboarding.test.ts src/__tests__/model-registry-defaults.test.ts src/cli/args.test.ts",
|
|
56
|
+
"test:coverage": "vitest run --coverage src/extensions/undo.test.ts src/extensions/tps.test.ts src/extensions/agent-browser.test.ts src/extensions/auto-session-name.test.ts src/extensions/context-compaction-reminder.test.ts src/extensions/copy-turn.test.ts src/extensions/grep-app/index.test.ts src/extensions/inline-skills.test.ts src/extensions/rtk.test.ts src/extensions/handoff-new.test.ts src/extensions/question/tests src/extensions/ponytail/test src/__tests__/tokenin-onboarding.test.ts src/__tests__/model-registry-defaults.test.ts src/cli/args.test.ts",
|
|
57
57
|
"prepare": "npm run build",
|
|
58
58
|
"build": "npm run clean && tsgo -p tsconfig.build.json && shx chmod +x dist/cli.js dist/rpc-entry.js && npm run copy-assets",
|
|
59
59
|
"copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/modes/interactive/assets && shx cp src/modes/interactive/assets/*.png dist/modes/interactive/assets/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/ && shx mkdir -p dist/defaults && shx cp src/defaults/* dist/defaults/ && shx mkdir -p dist/extensions && node scripts/copy-extensions.mjs && shx mkdir -p dist/themes && shx cp -r src/themes/. dist/themes/ && shx mkdir -p dist/skills && shx cp -r src/skills/. dist/skills/"
|
|
@@ -1,415 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
-
|
|
6
|
-
vi.mock("@selesai/code", () => ({
|
|
7
|
-
getAgentDir: () => process.env.SELESAI_CODING_AGENT_DIR ?? join(require("node:os").homedir(), ".selesai", "agent"),
|
|
8
|
-
getSettingsPath: () => join(process.env.SELESAI_CODING_AGENT_DIR ?? join(require("node:os").homedir(), ".selesai", "agent"), "settings.json"),
|
|
9
|
-
}));
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
BRAVE_KEYS_URL,
|
|
13
|
-
getBraveApiKey,
|
|
14
|
-
getWebAgentConfigPath,
|
|
15
|
-
isPlaceholderKey,
|
|
16
|
-
ONBOARDING_MARKER_NAME,
|
|
17
|
-
readSettingsJson,
|
|
18
|
-
setBraveApiKey,
|
|
19
|
-
validateBraveKey,
|
|
20
|
-
WEB_AGENT_CONFIG_SUBPATH,
|
|
21
|
-
writeSettingsJson,
|
|
22
|
-
writeWebAgentBraveBackendConfig,
|
|
23
|
-
} from "./web-agent-onboarding.ts";
|
|
24
|
-
import webAgentOnboardingExtension from "./web-agent-onboarding.ts";
|
|
25
|
-
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, SessionStartEvent } from "../core/extensions/types.ts";
|
|
26
|
-
|
|
27
|
-
let agentDir: string;
|
|
28
|
-
|
|
29
|
-
beforeEach(() => {
|
|
30
|
-
agentDir = mkdtempSync(join(tmpdir(), "web-agent-ob-"));
|
|
31
|
-
process.env.SELESAI_CODING_AGENT_DIR = agentDir;
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
afterEach(() => {
|
|
35
|
-
rmSync(agentDir, { recursive: true, force: true });
|
|
36
|
-
delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe("web-agent-onboarding helpers", () => {
|
|
40
|
-
it("isPlaceholderKey returns true for missing/empty/non-string values", () => {
|
|
41
|
-
expect(isPlaceholderKey(undefined)).toBe(true);
|
|
42
|
-
expect(isPlaceholderKey(null)).toBe(true);
|
|
43
|
-
expect(isPlaceholderKey("")).toBe(true);
|
|
44
|
-
expect(isPlaceholderKey(" ")).toBe(true);
|
|
45
|
-
expect(isPlaceholderKey(42)).toBe(true);
|
|
46
|
-
expect(isPlaceholderKey("BSA123abc")).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("getBraveApiKey reads the nested value safely", () => {
|
|
50
|
-
expect(getBraveApiKey({})).toBeUndefined();
|
|
51
|
-
expect(getBraveApiKey({ webAgent: "not-an-object" })).toBeUndefined();
|
|
52
|
-
expect(getBraveApiKey({ webAgent: [] })).toBeUndefined();
|
|
53
|
-
expect(getBraveApiKey({ webAgent: { braveApiKey: "key-1" } })).toBe("key-1");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("setBraveApiKey clones and sets the nested value", () => {
|
|
57
|
-
const original = { other: 1, webAgent: { braveApiKey: "old" } };
|
|
58
|
-
const next = setBraveApiKey(original, "new-key");
|
|
59
|
-
expect(next.webAgent).toEqual({ braveApiKey: "new-key" });
|
|
60
|
-
expect(next.other).toBe(1);
|
|
61
|
-
// original is not mutated
|
|
62
|
-
expect(original.webAgent.braveApiKey).toBe("old");
|
|
63
|
-
// missing/non-object webAgent is replaced
|
|
64
|
-
expect(setBraveApiKey({}, "k").webAgent).toEqual({ braveApiKey: "k" });
|
|
65
|
-
expect(setBraveApiKey({ webAgent: "x" }, "k").webAgent).toEqual({ braveApiKey: "k" });
|
|
66
|
-
expect(setBraveApiKey({ webAgent: [] }, "k").webAgent).toEqual({ braveApiKey: "k" });
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("validateBraveKey trims and reports errors", () => {
|
|
70
|
-
expect(validateBraveKey(" BSA-key ")).toEqual({ key: "BSA-key" });
|
|
71
|
-
expect(validateBraveKey("")).toEqual({ error: "Key cannot be empty." });
|
|
72
|
-
expect(validateBraveKey(" ")).toEqual({ error: "Key cannot be empty." });
|
|
73
|
-
expect(validateBraveKey(undefined)).toEqual({ error: "No key entered." });
|
|
74
|
-
expect(validateBraveKey(null as unknown as string)).toEqual({ error: "No key entered." });
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("readSettingsJson parses existing file, defaults to {} for missing/corrupt", () => {
|
|
78
|
-
const settingsPath = join(agentDir, "settings.json");
|
|
79
|
-
writeFileSync(settingsPath, '{"webAgent":{"braveApiKey":"k"}}', "utf-8");
|
|
80
|
-
expect(readSettingsJson(settingsPath)).toEqual({ webAgent: { braveApiKey: "k" } });
|
|
81
|
-
expect(readSettingsJson(join(agentDir, "missing.json"))).toEqual({});
|
|
82
|
-
writeFileSync(settingsPath, "{not json", "utf-8");
|
|
83
|
-
expect(readSettingsJson(settingsPath)).toEqual({});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("writeSettingsJson creates dirs and writes pretty JSON with trailing newline", () => {
|
|
87
|
-
const deep = join(agentDir, "nested", "dir");
|
|
88
|
-
const settingsPath = join(deep, "settings.json");
|
|
89
|
-
writeSettingsJson({ a: 1 }, settingsPath);
|
|
90
|
-
expect(readFileSync(settingsPath, "utf-8")).toBe('{\n "a": 1\n}\n');
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("getWebAgentConfigPath joins agentDir with the config subpath", () => {
|
|
94
|
-
expect(getWebAgentConfigPath(agentDir)).toBe(join(agentDir, "extensions", "pi-web-agent", "config.json"));
|
|
95
|
-
expect(WEB_AGENT_CONFIG_SUBPATH).toBe(join("extensions", "pi-web-agent", "config.json"));
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("writeWebAgentBraveBackendConfig writes brave with duckduckgo fallback, merging existing", () => {
|
|
99
|
-
const configPath = getWebAgentConfigPath(agentDir);
|
|
100
|
-
// Pre-seed an existing config to verify merge-not-clobber.
|
|
101
|
-
mkdirSync(join(agentDir, "extensions", "pi-web-agent"), { recursive: true });
|
|
102
|
-
writeFileSync(configPath, '{"presentation":{"theme":"dark"}}', "utf-8");
|
|
103
|
-
writeWebAgentBraveBackendConfig(agentDir);
|
|
104
|
-
const parsed = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
105
|
-
expect(parsed.presentation).toEqual({ theme: "dark" });
|
|
106
|
-
expect(parsed.backends.search).toEqual({ provider: "brave", fallback: "duckduckgo" });
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("writeWebAgentBraveBackendConfig tolerates missing/corrupt existing config", () => {
|
|
110
|
-
const configPath = getWebAgentConfigPath(agentDir);
|
|
111
|
-
mkdirSync(join(agentDir, "extensions", "pi-web-agent"), { recursive: true });
|
|
112
|
-
writeFileSync(configPath, "{corrupt", "utf-8");
|
|
113
|
-
writeWebAgentBraveBackendConfig(agentDir);
|
|
114
|
-
const parsed = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
115
|
-
expect(parsed.backends.search).toEqual({ provider: "brave", fallback: "duckduckgo" });
|
|
116
|
-
|
|
117
|
-
// Missing file entirely -> starts fresh.
|
|
118
|
-
const freshDir = mkdtempSync(join(tmpdir(), "web-agent-fresh-"));
|
|
119
|
-
writeWebAgentBraveBackendConfig(freshDir);
|
|
120
|
-
const freshParsed = JSON.parse(readFileSync(join(freshDir, WEB_AGENT_CONFIG_SUBPATH), "utf-8"));
|
|
121
|
-
expect(freshParsed.backends.search).toEqual({ provider: "brave", fallback: "duckduckgo" });
|
|
122
|
-
rmSync(freshDir, { recursive: true, force: true });
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
describe("web-agent-onboarding constants and markers", () => {
|
|
127
|
-
it("exports the Brave keys URL and marker name", () => {
|
|
128
|
-
expect(BRAVE_KEYS_URL).toContain("brave.com");
|
|
129
|
-
expect(ONBOARDING_MARKER_NAME).toBe(".webAgentOnboardingComplete");
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe("web-agent-onboarding extension lifecycle", () => {
|
|
134
|
-
function makePi(overrides: Partial<ExtensionAPI> = {}): { pi: ExtensionAPI; on: ReturnType<typeof vi.fn>; registerCommand: ReturnType<typeof vi.fn> } {
|
|
135
|
-
const on = vi.fn();
|
|
136
|
-
const registerCommand = vi.fn();
|
|
137
|
-
const pi = { on, registerCommand, ...overrides } as unknown as ExtensionAPI;
|
|
138
|
-
webAgentOnboardingExtension(pi);
|
|
139
|
-
return { pi, on, registerCommand };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function sessionStartHandler(pi: ExtensionAPI): (event: SessionStartEvent, ctx: ExtensionContext) => Promise<void> {
|
|
143
|
-
return (pi.on as ReturnType<typeof vi.fn>).mock.calls.find(([event]: [string]) => event === "session_start")![1];
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
it("registers session_start and setup-web-search command", () => {
|
|
147
|
-
const { on, registerCommand } = makePi();
|
|
148
|
-
expect(on).toHaveBeenCalledWith("session_start", expect.any(Function));
|
|
149
|
-
expect(registerCommand).toHaveBeenCalledWith("setup-web-search", expect.objectContaining({ description: expect.stringContaining("Brave") }));
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("session_start ignores non-startup reasons", async () => {
|
|
153
|
-
const { pi } = makePi();
|
|
154
|
-
const handler = sessionStartHandler(pi);
|
|
155
|
-
const ctx = { hasUI: true } as unknown as ExtensionContext;
|
|
156
|
-
await handler({ reason: "reload" } as SessionStartEvent, ctx);
|
|
157
|
-
// No marker written, no prompt: marker missing proves no completion occurred.
|
|
158
|
-
expect(existsSync(join(agentDir, ONBOARDING_MARKER_NAME))).toBe(false);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("session_start skips when onboarding is already complete", async () => {
|
|
162
|
-
writeFileSync(join(agentDir, ONBOARDING_MARKER_NAME), "1", "utf-8");
|
|
163
|
-
const { pi } = makePi();
|
|
164
|
-
const handler = sessionStartHandler(pi);
|
|
165
|
-
const input = vi.fn();
|
|
166
|
-
const ctx = { hasUI: true, ui: { input } } as unknown as ExtensionContext;
|
|
167
|
-
await handler({ reason: "startup" } as SessionStartEvent, ctx);
|
|
168
|
-
expect(input).not.toHaveBeenCalled();
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("session_start defers when SELESAI_SKIP_WEB_AGENT_ONBOARDING=1", async () => {
|
|
172
|
-
process.env.SELESAI_SKIP_WEB_AGENT_ONBOARDING = "1";
|
|
173
|
-
try {
|
|
174
|
-
const { pi } = makePi();
|
|
175
|
-
const handler = sessionStartHandler(pi);
|
|
176
|
-
const input = vi.fn();
|
|
177
|
-
const ctx = { hasUI: true, ui: { input } } as unknown as ExtensionContext;
|
|
178
|
-
await handler({ reason: "startup" } as SessionStartEvent, ctx);
|
|
179
|
-
expect(input).not.toHaveBeenCalled();
|
|
180
|
-
} finally {
|
|
181
|
-
delete process.env.SELESAI_SKIP_WEB_AGENT_ONBOARDING;
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it("session_start skips when no UI is available", async () => {
|
|
186
|
-
const { pi } = makePi();
|
|
187
|
-
const handler = sessionStartHandler(pi);
|
|
188
|
-
const input = vi.fn();
|
|
189
|
-
const ctx = { hasUI: false, ui: { input } } as unknown as ExtensionContext;
|
|
190
|
-
await handler({ reason: "startup" } as SessionStartEvent, ctx);
|
|
191
|
-
expect(input).not.toHaveBeenCalled();
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("already-configured key defers and marks complete", async () => {
|
|
195
|
-
const settingsPath = join(agentDir, "settings.json");
|
|
196
|
-
writeFileSync(settingsPath, '{"webAgent":{"braveApiKey":"existing-key"}}', "utf-8");
|
|
197
|
-
const { pi } = makePi();
|
|
198
|
-
const handler = sessionStartHandler(pi);
|
|
199
|
-
const confirm = vi.fn();
|
|
200
|
-
const ctx = { hasUI: true, ui: { confirm } } as unknown as ExtensionContext;
|
|
201
|
-
await handler({ reason: "startup" } as SessionStartEvent, ctx);
|
|
202
|
-
expect(confirm).not.toHaveBeenCalled();
|
|
203
|
-
expect(existsSync(join(agentDir, ONBOARDING_MARKER_NAME))).toBe(true);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("declining Brave skips onboarding and notifies", async () => {
|
|
207
|
-
const { pi } = makePi();
|
|
208
|
-
const handler = sessionStartHandler(pi);
|
|
209
|
-
const notify = vi.fn();
|
|
210
|
-
const ctx = { hasUI: true, ui: { confirm: async () => false, notify } } as unknown as ExtensionContext;
|
|
211
|
-
await handler({ reason: "startup" } as SessionStartEvent, ctx);
|
|
212
|
-
expect(notify).toHaveBeenCalledWith(expect.stringContaining("skipped"), "info");
|
|
213
|
-
expect(existsSync(join(agentDir, ONBOARDING_MARKER_NAME))).toBe(true);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it("accepting Brave saves the key and config", async () => {
|
|
217
|
-
const { pi } = makePi();
|
|
218
|
-
const handler = sessionStartHandler(pi);
|
|
219
|
-
const notify = vi.fn();
|
|
220
|
-
const exec = vi.fn().mockResolvedValue({ code: 0, stdout: "", stderr: "" });
|
|
221
|
-
const ctx = {
|
|
222
|
-
hasUI: true,
|
|
223
|
-
ui: { confirm: async () => true, input: async () => " BSA-my-key ", notify },
|
|
224
|
-
} as unknown as ExtensionContext;
|
|
225
|
-
await handler({ reason: "startup" } as SessionStartEvent, ctx);
|
|
226
|
-
expect(notify).toHaveBeenCalledWith(expect.stringContaining("saved"), "info");
|
|
227
|
-
expect(exec).not.toHaveBeenCalled(); // openBraveKeysUrl uses pi.exec — not provided here
|
|
228
|
-
const settings = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf-8"));
|
|
229
|
-
expect(settings.webAgent.braveApiKey).toBe("BSA-my-key");
|
|
230
|
-
const config = JSON.parse(readFileSync(join(agentDir, WEB_AGENT_CONFIG_SUBPATH), "utf-8"));
|
|
231
|
-
expect(config.backends.search).toEqual({ provider: "brave", fallback: "duckduckgo" });
|
|
232
|
-
expect(existsSync(join(agentDir, ONBOARDING_MARKER_NAME))).toBe(true);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it("accepting Brave with browser open failure still saves", async () => {
|
|
236
|
-
const { pi } = makePi();
|
|
237
|
-
const handler = sessionStartHandler(pi);
|
|
238
|
-
const notify = vi.fn();
|
|
239
|
-
const exec = vi.fn().mockResolvedValue({ code: 1, stdout: "", stderr: "no browser" });
|
|
240
|
-
const ctx = {
|
|
241
|
-
hasUI: true,
|
|
242
|
-
ui: { confirm: async () => true, input: async () => "BSA-key", notify },
|
|
243
|
-
} as unknown as ExtensionContext;
|
|
244
|
-
await handler({ reason: "startup" } as SessionStartEvent, ctx);
|
|
245
|
-
expect(notify).toHaveBeenCalledWith(expect.stringContaining("Could not open browser"), "warning");
|
|
246
|
-
expect(notify).toHaveBeenCalledWith(expect.stringContaining("manually"), "info");
|
|
247
|
-
const settings = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf-8"));
|
|
248
|
-
expect(settings.webAgent.braveApiKey).toBe("BSA-key");
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
describe("openBraveKeysUrl platform branches", () => {
|
|
252
|
-
it("uses win32 command and still saves on zero exit", async () => {
|
|
253
|
-
const original = process.platform;
|
|
254
|
-
const exec = vi.fn().mockResolvedValue({ code: 0, stdout: "", stderr: "" });
|
|
255
|
-
try {
|
|
256
|
-
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
|
257
|
-
const { pi } = makePi({ exec } as unknown as ExtensionAPI);
|
|
258
|
-
const handler = sessionStartHandler(pi);
|
|
259
|
-
const notify = vi.fn();
|
|
260
|
-
const ctx = {
|
|
261
|
-
hasUI: true,
|
|
262
|
-
ui: { confirm: async () => true, input: async () => "BSA-win", notify },
|
|
263
|
-
} as unknown as ExtensionContext;
|
|
264
|
-
await handler({ reason: "startup" } as SessionStartEvent, ctx);
|
|
265
|
-
expect(exec).toHaveBeenCalledWith("cmd", ["/c", "start", "", BRAVE_KEYS_URL], { timeout: 10_000 });
|
|
266
|
-
expect(notify).toHaveBeenCalledWith(expect.stringContaining("Opened the Brave API key dashboard"), "info");
|
|
267
|
-
} finally {
|
|
268
|
-
Object.defineProperty(process, "platform", { value: original, configurable: true });
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it("uses xdg-open on other platforms", async () => {
|
|
273
|
-
const original = process.platform;
|
|
274
|
-
const exec = vi.fn().mockResolvedValue({ code: 0, stdout: "", stderr: "" });
|
|
275
|
-
try {
|
|
276
|
-
Object.defineProperty(process, "platform", { value: "linux", configurable: true });
|
|
277
|
-
const { pi } = makePi({ exec } as unknown as ExtensionAPI);
|
|
278
|
-
const handler = sessionStartHandler(pi);
|
|
279
|
-
const notify = vi.fn();
|
|
280
|
-
const ctx = {
|
|
281
|
-
hasUI: true,
|
|
282
|
-
ui: { confirm: async () => true, input: async () => "BSA-lin", notify },
|
|
283
|
-
} as unknown as ExtensionContext;
|
|
284
|
-
await handler({ reason: "startup" } as SessionStartEvent, ctx);
|
|
285
|
-
expect(exec).toHaveBeenCalledWith("xdg-open", [BRAVE_KEYS_URL], { timeout: 10_000 });
|
|
286
|
-
} finally {
|
|
287
|
-
Object.defineProperty(process, "platform", { value: original, configurable: true });
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it("non-zero exit warns and opens the manual URL even after a string error", async () => {
|
|
292
|
-
const original = process.platform;
|
|
293
|
-
const exec = vi.fn().mockResolvedValue({ code: 1, stdout: "", stderr: "browser failed" });
|
|
294
|
-
try {
|
|
295
|
-
Object.defineProperty(process, "platform", { value: "darwin", configurable: true });
|
|
296
|
-
const { pi } = makePi({ exec } as unknown as ExtensionAPI);
|
|
297
|
-
const handler = sessionStartHandler(pi);
|
|
298
|
-
const notify = vi.fn();
|
|
299
|
-
const ctx = {
|
|
300
|
-
hasUI: true,
|
|
301
|
-
ui: { confirm: async () => true, input: async () => "BSA-err", notify },
|
|
302
|
-
} as unknown as ExtensionContext;
|
|
303
|
-
await handler({ reason: "startup" } as SessionStartEvent, ctx);
|
|
304
|
-
expect(notify).toHaveBeenCalledWith(expect.stringContaining("Could not open browser: Failed to open"), "warning");
|
|
305
|
-
expect(notify).toHaveBeenCalledWith(expect.stringContaining("manually"), "info");
|
|
306
|
-
// Even though the browser failed, the key is still saved.
|
|
307
|
-
const settings = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf-8"));
|
|
308
|
-
expect(settings.webAgent.braveApiKey).toBe("BSA-err");
|
|
309
|
-
} finally {
|
|
310
|
-
Object.defineProperty(process, "platform", { value: original, configurable: true });
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it("a rejecting exec (non-Error) still warns without crashing the flow", async () => {
|
|
315
|
-
const original = process.platform;
|
|
316
|
-
const exec = vi.fn().mockRejectedValue("plain string failure");
|
|
317
|
-
try {
|
|
318
|
-
Object.defineProperty(process, "platform", { value: "darwin", configurable: true });
|
|
319
|
-
const { pi } = makePi({ exec } as unknown as ExtensionAPI);
|
|
320
|
-
const handler = sessionStartHandler(pi);
|
|
321
|
-
const notify = vi.fn();
|
|
322
|
-
const ctx = {
|
|
323
|
-
hasUI: true,
|
|
324
|
-
ui: { confirm: async () => true, input: async () => "BSA-rej", notify },
|
|
325
|
-
} as unknown as ExtensionContext;
|
|
326
|
-
await handler({ reason: "startup" } as SessionStartEvent, ctx);
|
|
327
|
-
expect(notify).toHaveBeenCalledWith(expect.stringContaining("Could not open browser: plain string failure"), "warning");
|
|
328
|
-
const settings = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf-8"));
|
|
329
|
-
expect(settings.webAgent.braveApiKey).toBe("BSA-rej");
|
|
330
|
-
} finally {
|
|
331
|
-
Object.defineProperty(process, "platform", { value: original, configurable: true });
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
it("non-zero exit with both stderr and stdout empty falls back to unknown error", async () => {
|
|
335
|
-
const original = process.platform;
|
|
336
|
-
const exec = vi.fn().mockResolvedValue({ code: 1, stdout: "", stderr: "" });
|
|
337
|
-
try {
|
|
338
|
-
Object.defineProperty(process, "platform", { value: "darwin", configurable: true });
|
|
339
|
-
const { pi } = makePi({ exec } as unknown as ExtensionAPI);
|
|
340
|
-
const handler = sessionStartHandler(pi);
|
|
341
|
-
const notify = vi.fn();
|
|
342
|
-
const ctx = {
|
|
343
|
-
hasUI: true,
|
|
344
|
-
ui: { confirm: async () => true, input: async () => "BSA-un", notify },
|
|
345
|
-
} as unknown as ExtensionContext;
|
|
346
|
-
await handler({ reason: "startup" } as SessionStartEvent, ctx);
|
|
347
|
-
expect(notify).toHaveBeenCalledWith(expect.stringContaining("unknown error"), "warning");
|
|
348
|
-
const settings = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf-8"));
|
|
349
|
-
expect(settings.webAgent.braveApiKey).toBe("BSA-un");
|
|
350
|
-
} finally {
|
|
351
|
-
Object.defineProperty(process, "platform", { value: original, configurable: true });
|
|
352
|
-
}
|
|
353
|
-
});
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
it("empty pasted key defers without saving", async () => {
|
|
357
|
-
const { pi } = makePi();
|
|
358
|
-
const handler = sessionStartHandler(pi);
|
|
359
|
-
const notify = vi.fn();
|
|
360
|
-
const ctx = {
|
|
361
|
-
hasUI: true,
|
|
362
|
-
ui: { confirm: async () => true, input: async () => " ", notify },
|
|
363
|
-
} as unknown as ExtensionContext;
|
|
364
|
-
await handler({ reason: "startup" } as SessionStartEvent, ctx);
|
|
365
|
-
expect(notify).toHaveBeenCalledWith("Key cannot be empty.", "warning");
|
|
366
|
-
expect(existsSync(join(agentDir, ONBOARDING_MARKER_NAME))).toBe(false);
|
|
367
|
-
expect(existsSync(join(agentDir, "settings.json"))).toBe(false);
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
it("setup-web-search command saves and reloads extensions", async () => {
|
|
371
|
-
const exec = vi.fn().mockResolvedValue({ code: 0, stdout: "", stderr: "" });
|
|
372
|
-
const { registerCommand } = makePi({ exec } as unknown as ExtensionAPI);
|
|
373
|
-
const { handler } = registerCommand.mock.calls.find(([name]: [string]) => name === "setup-web-search")![1] as { handler: (args: string, ctx: ExtensionCommandContext) => Promise<void> };
|
|
374
|
-
const reload = vi.fn().mockResolvedValue(undefined);
|
|
375
|
-
const notify = vi.fn();
|
|
376
|
-
const ctx = {
|
|
377
|
-
hasUI: true,
|
|
378
|
-
reload,
|
|
379
|
-
ui: { confirm: async () => true, input: async () => "BSA-key", notify },
|
|
380
|
-
} as unknown as ExtensionCommandContext;
|
|
381
|
-
await handler("", ctx);
|
|
382
|
-
expect(reload).toHaveBeenCalled();
|
|
383
|
-
expect(notify).toHaveBeenCalledWith(expect.stringContaining("reloading extensions"), "info");
|
|
384
|
-
const settings = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf-8"));
|
|
385
|
-
expect(settings.webAgent.braveApiKey).toBe("BSA-key");
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it("setup-web-search command does not reload when skipped", async () => {
|
|
389
|
-
const { registerCommand } = makePi();
|
|
390
|
-
const { handler } = registerCommand.mock.calls.find(([name]: [string]) => name === "setup-web-search")![1] as { handler: (args: string, ctx: ExtensionCommandContext) => Promise<void> };
|
|
391
|
-
const reload = vi.fn();
|
|
392
|
-
const notify = vi.fn();
|
|
393
|
-
const ctx = {
|
|
394
|
-
hasUI: true,
|
|
395
|
-
reload,
|
|
396
|
-
ui: { confirm: async () => false, notify },
|
|
397
|
-
} as unknown as ExtensionCommandContext;
|
|
398
|
-
await handler("", ctx);
|
|
399
|
-
expect(reload).not.toHaveBeenCalled();
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it("setup-web-search command defers (no reload) when key is invalid", async () => {
|
|
403
|
-
const { registerCommand } = makePi();
|
|
404
|
-
const { handler } = registerCommand.mock.calls.find(([name]: [string]) => name === "setup-web-search")![1] as { handler: (args: string, ctx: ExtensionCommandContext) => Promise<void> };
|
|
405
|
-
const reload = vi.fn();
|
|
406
|
-
const notify = vi.fn();
|
|
407
|
-
const ctx = {
|
|
408
|
-
hasUI: true,
|
|
409
|
-
reload,
|
|
410
|
-
ui: { confirm: async () => true, input: async () => "", notify },
|
|
411
|
-
} as unknown as ExtensionCommandContext;
|
|
412
|
-
await handler("", ctx);
|
|
413
|
-
expect(reload).not.toHaveBeenCalled();
|
|
414
|
-
});
|
|
415
|
-
});
|
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Web-Agent first-run onboarding extension.
|
|
3
|
-
*
|
|
4
|
-
* On the first interactive session, ask the user whether they want to use Brave
|
|
5
|
-
* Search. If yes, open the Brave API key dashboard, collect the pasted key, and
|
|
6
|
-
* persist it in ~/.selesai/agent/settings.json under webAgent.braveApiKey, then
|
|
7
|
-
* configure the bundled pi-web-agent backend to use Brave (with DuckDuckGo fallback).
|
|
8
|
-
* If no, the extension falls back to pi-web-agent's default DuckDuckGo backend.
|
|
9
|
-
*
|
|
10
|
-
* Mirrors src/extensions/tokenin-onboarding.ts.
|
|
11
|
-
*/
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
-
import { dirname, join } from "node:path";
|
|
14
|
-
import { getAgentDir, getSettingsPath } from "@selesai/code";
|
|
15
|
-
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, SessionStartEvent } from "@selesai/code";
|
|
16
|
-
|
|
17
|
-
export type OnboardingResult =
|
|
18
|
-
| { kind: "saved" }
|
|
19
|
-
| { kind: "skipped" }
|
|
20
|
-
| { kind: "deferred" }
|
|
21
|
-
| { kind: "error"; message: string };
|
|
22
|
-
|
|
23
|
-
export const BRAVE_KEYS_URL = "https://api-dashboard.search.brave.com/app/keys";
|
|
24
|
-
export const ONBOARDING_MARKER_NAME = ".webAgentOnboardingComplete";
|
|
25
|
-
export const WEB_AGENT_CONFIG_SUBPATH = join("extensions", "pi-web-agent", "config.json");
|
|
26
|
-
|
|
27
|
-
// ponytail: duplicated ~15 lines of marker-file + browser-open logic from
|
|
28
|
-
// tokenin-onboarding.ts rather than extracting a shared module nobody asked for.
|
|
29
|
-
// Ceiling: if a third onboarding extension appears, extract a shared helper then.
|
|
30
|
-
|
|
31
|
-
/** Read and parse the current settings.json, returning a deep-ish mutable object. */
|
|
32
|
-
export function readSettingsJson(settingsPath: string = getSettingsPath()): Record<string, unknown> {
|
|
33
|
-
try {
|
|
34
|
-
const raw = readFileSync(settingsPath, "utf-8");
|
|
35
|
-
return JSON.parse(raw) as Record<string, unknown>;
|
|
36
|
-
} catch {
|
|
37
|
-
return {};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Write settings.json back to disk atomically-ish with a trailing newline. */
|
|
42
|
-
export function writeSettingsJson(settings: Record<string, unknown>, settingsPath: string = getSettingsPath()): void {
|
|
43
|
-
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
44
|
-
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Return true if the Brave key value is missing, empty. We prompt again for any
|
|
49
|
-
* non-key value.
|
|
50
|
-
*/
|
|
51
|
-
export function isPlaceholderKey(apiKey: unknown): boolean {
|
|
52
|
-
if (apiKey === undefined || apiKey === null) return true;
|
|
53
|
-
if (typeof apiKey !== "string") return true;
|
|
54
|
-
return apiKey.trim() === "";
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Return the current webAgent.braveApiKey value from settings.json. */
|
|
58
|
-
export function getBraveApiKey(settings: Record<string, unknown>): unknown {
|
|
59
|
-
const webAgent = settings.webAgent;
|
|
60
|
-
if (!webAgent || typeof webAgent !== "object" || Array.isArray(webAgent)) return undefined;
|
|
61
|
-
return (webAgent as Record<string, unknown>).braveApiKey;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Return a new settings object with webAgent.braveApiKey set to key. */
|
|
65
|
-
export function setBraveApiKey(settings: Record<string, unknown>, key: string): Record<string, unknown> {
|
|
66
|
-
const next = JSON.parse(JSON.stringify(settings)) as Record<string, unknown>;
|
|
67
|
-
if (!next.webAgent || typeof next.webAgent !== "object" || Array.isArray(next.webAgent)) {
|
|
68
|
-
next.webAgent = {};
|
|
69
|
-
}
|
|
70
|
-
const webAgent = next.webAgent as Record<string, unknown>;
|
|
71
|
-
webAgent.braveApiKey = key;
|
|
72
|
-
return next;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Validate a pasted Brave key. Returns normalized key or an error message. */
|
|
76
|
-
export function validateBraveKey(raw: string | undefined): { key?: string; error?: string } {
|
|
77
|
-
if (raw === undefined || raw === null) return { error: "No key entered." };
|
|
78
|
-
const trimmed = raw.trim();
|
|
79
|
-
if (trimmed === "") return { error: "Key cannot be empty." };
|
|
80
|
-
return { key: trimmed };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Path to the pi-web-agent global backend config (matches vendored config-store). */
|
|
84
|
-
export function getWebAgentConfigPath(agentDir: string = getAgentDir()): string {
|
|
85
|
-
return join(agentDir, WEB_AGENT_CONFIG_SUBPATH);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Write pi-web-agent backend config selecting brave (with duckduckgo fallback). */
|
|
89
|
-
export function writeWebAgentBraveBackendConfig(agentDir: string = getAgentDir()): void {
|
|
90
|
-
const configPath = getWebAgentConfigPath(agentDir);
|
|
91
|
-
mkdirSync(dirname(configPath), { recursive: true });
|
|
92
|
-
// Merge into any existing config so we don't clobber presentation settings.
|
|
93
|
-
let existing: Record<string, unknown> = {};
|
|
94
|
-
try {
|
|
95
|
-
existing = JSON.parse(readFileSync(configPath, "utf-8")) as Record<string, unknown>;
|
|
96
|
-
} catch {
|
|
97
|
-
// ENOENT or invalid JSON — start fresh.
|
|
98
|
-
}
|
|
99
|
-
const next = existing;
|
|
100
|
-
const backends = (next.backends ?? {}) as Record<string, unknown>;
|
|
101
|
-
(backends as Record<string, unknown>).search = { provider: "brave", fallback: "duckduckgo" };
|
|
102
|
-
next.backends = backends;
|
|
103
|
-
writeFileSync(configPath, `${JSON.stringify(next, null, 2)}\n`, "utf-8");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function getOnboardingMarkerPath(agentDir: string = getAgentDir()): string {
|
|
107
|
-
return join(agentDir, ONBOARDING_MARKER_NAME);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function markOnboardingComplete(agentDir: string = getAgentDir()): void {
|
|
111
|
-
writeFileSync(getOnboardingMarkerPath(agentDir), "1", "utf-8");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function isOnboardingComplete(agentDir: string = getAgentDir()): boolean {
|
|
115
|
-
return existsSync(getOnboardingMarkerPath(agentDir));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function openBraveKeysUrl(pi: ExtensionAPI): Promise<void> {
|
|
119
|
-
const url = BRAVE_KEYS_URL;
|
|
120
|
-
let command: string;
|
|
121
|
-
let args: string[];
|
|
122
|
-
if (process.platform === "darwin") {
|
|
123
|
-
command = "open";
|
|
124
|
-
args = [url];
|
|
125
|
-
} else if (process.platform === "win32") {
|
|
126
|
-
command = "cmd";
|
|
127
|
-
args = ["/c", "start", "", url];
|
|
128
|
-
} else {
|
|
129
|
-
command = "xdg-open";
|
|
130
|
-
args = [url];
|
|
131
|
-
}
|
|
132
|
-
const result = await pi.exec(command, args, { timeout: 10_000 });
|
|
133
|
-
if (result.code !== 0) {
|
|
134
|
-
throw new Error(`Failed to open ${url}: ${result.stderr || result.stdout || "unknown error"}`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function promptForKey(ctx: ExtensionContext): Promise<string | undefined> {
|
|
139
|
-
return ctx.ui.input("Paste your Brave API key", "");
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function runOnboarding(pi: ExtensionAPI, ctx: ExtensionContext): Promise<OnboardingResult> {
|
|
143
|
-
const settingsPath = getSettingsPath();
|
|
144
|
-
const settings = readSettingsJson(settingsPath);
|
|
145
|
-
const currentKey = getBraveApiKey(settings);
|
|
146
|
-
|
|
147
|
-
// Already configured? Mark complete and skip.
|
|
148
|
-
if (!isPlaceholderKey(currentKey)) {
|
|
149
|
-
markOnboardingComplete();
|
|
150
|
-
return { kind: "deferred" };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// No UI? Defer to a later session.
|
|
154
|
-
if (!ctx.hasUI) {
|
|
155
|
-
return { kind: "deferred" };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const wantsBrave = await ctx.ui.confirm(
|
|
159
|
-
"Use Brave Search?",
|
|
160
|
-
"Brave gives reliable web results for the web_explore tool. You can get an API key at https://api-dashboard.search.brave.com/app/keys. Use Brave now?",
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
if (!wantsBrave) {
|
|
164
|
-
markOnboardingComplete();
|
|
165
|
-
ctx.ui.notify("Brave skipped — web search will use the default DuckDuckGo backend.", "info");
|
|
166
|
-
return { kind: "skipped" };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
await openBraveKeysUrl(pi);
|
|
171
|
-
ctx.ui.notify("Opened the Brave API key dashboard in your browser.", "info");
|
|
172
|
-
} catch (err) {
|
|
173
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
174
|
-
ctx.ui.notify(`Could not open browser: ${message}`, "warning");
|
|
175
|
-
ctx.ui.notify(`Open this URL manually: ${BRAVE_KEYS_URL}`, "info");
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const rawKey = await promptForKey(ctx);
|
|
179
|
-
const validated = validateBraveKey(rawKey);
|
|
180
|
-
|
|
181
|
-
if (validated.error) {
|
|
182
|
-
ctx.ui.notify(validated.error, "warning");
|
|
183
|
-
// Do not mark complete so the prompt retries on next launch.
|
|
184
|
-
return { kind: "deferred" };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const updated = setBraveApiKey(settings, validated.key!);
|
|
188
|
-
writeSettingsJson(updated, settingsPath);
|
|
189
|
-
// Select Brave as the pi-web-agent search backend (DuckDuckGo fallback).
|
|
190
|
-
writeWebAgentBraveBackendConfig();
|
|
191
|
-
markOnboardingComplete();
|
|
192
|
-
|
|
193
|
-
ctx.ui.notify("Brave API key saved — web search will use Brave.", "info");
|
|
194
|
-
return { kind: "saved" };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export default function webAgentOnboardingExtension(pi: ExtensionAPI): void {
|
|
198
|
-
pi.on("session_start", async (event: SessionStartEvent, ctx: ExtensionContext) => {
|
|
199
|
-
if (event.reason !== "startup") return;
|
|
200
|
-
if (isOnboardingComplete()) return;
|
|
201
|
-
|
|
202
|
-
// Avoid prompting during tests or non-interactive runs.
|
|
203
|
-
if (process.env.SELESAI_SKIP_WEB_AGENT_ONBOARDING === "1") return;
|
|
204
|
-
|
|
205
|
-
await runOnboarding(pi, ctx);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// ponytail: command reuses runOnboarding() instead of duplicating the flow.
|
|
209
|
-
// On "saved" -> ctx.reload() so pi-web-agent re-applies the Brave backend config.
|
|
210
|
-
// On skipped/deferred -> no reload (nothing changed). The post-reload session_start
|
|
211
|
-
// fires with reason "reload" but the auto handler only acts on "startup", so no double prompt.
|
|
212
|
-
pi.registerCommand("setup-web-search", {
|
|
213
|
-
description: "Re-run the Brave Search onboarding flow, then reload extensions",
|
|
214
|
-
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
215
|
-
const result = await runOnboarding(pi, ctx);
|
|
216
|
-
if (result.kind === "saved") {
|
|
217
|
-
ctx.ui.notify("Brave key saved — reloading extensions…", "info");
|
|
218
|
-
await ctx.reload();
|
|
219
|
-
}
|
|
220
|
-
},
|
|
221
|
-
});
|
|
222
|
-
}
|