pi-lean-dimension 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/node_modules/pi-lean-host/AGENTS.md +13 -6
- package/node_modules/pi-lean-host/__tests__/api-learn-fetch-recipe.test.ts +282 -10
- package/node_modules/pi-lean-host/__tests__/api-learn-multi-file.test.ts +8 -16
- package/node_modules/pi-lean-host/__tests__/api-probe.test.ts +276 -279
- package/node_modules/pi-lean-host/__tests__/api-scaffold.test.ts +12 -33
- package/node_modules/pi-lean-host/__tests__/api-toggle.test.ts +14 -12
- package/node_modules/pi-lean-host/__tests__/bootstrap-command.test.ts +1 -7
- package/node_modules/pi-lean-host/__tests__/delete-command.test.ts +1 -12
- package/node_modules/pi-lean-host/__tests__/guide-catalog.test.ts +484 -0
- package/node_modules/pi-lean-host/__tests__/helpers.test.ts +0 -132
- package/node_modules/pi-lean-host/__tests__/oauth-command.test.ts +95 -0
- package/node_modules/pi-lean-host/__tests__/oauth-flow.test.ts +163 -0
- package/node_modules/pi-lean-host/__tests__/oauth-mint.test.ts +79 -6
- package/node_modules/pi-lean-host/__tests__/parse-api-guide.test.ts +4 -409
- package/node_modules/pi-lean-host/__tests__/response-spill.test.ts +0 -1
- package/node_modules/pi-lean-host/__tests__/secrets-command.test.ts +19 -35
- package/node_modules/pi-lean-host/__tests__/smoke.test.ts +1 -82
- package/node_modules/pi-lean-host/__tests__/ssrf-guard.test.ts +98 -0
- package/node_modules/pi-lean-host/__tests__/test-utils.ts +73 -0
- package/node_modules/pi-lean-host/__tests__/tools.test.ts +2 -518
- package/node_modules/pi-lean-host/__tests__/transport.test.ts +178 -31
- package/node_modules/pi-lean-host/__tests__/verify-command.test.ts +1 -8
- package/node_modules/pi-lean-host/__tests__/verify-stamp.test.ts +1 -4
- package/node_modules/pi-lean-host/api-guides/boe/local-helper.test.ts +9 -20
- package/node_modules/pi-lean-host/api-guides/dnb/error-envelope.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/dnb/resumption-token.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/frost-sensorthings/dotted-key.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/github/static-key.test.ts +7 -25
- package/node_modules/pi-lean-host/api-guides/inaturalist/derived-id.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/internet-archive/multi-recipe.test.ts +6 -26
- package/node_modules/pi-lean-host/api-guides/stripe/has-more.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/telegram-bot/path-auth.test.ts +7 -25
- package/node_modules/pi-lean-host/api-guides/twitch/oauth2.test.ts +12 -47
- package/node_modules/pi-lean-host/api-guides/twitch-user/oauth-user.test.ts +11 -52
- package/node_modules/pi-lean-host/api-guides/usgs/transform.test.ts +10 -27
- package/node_modules/pi-lean-host/api-guides/wikidata-search/numeric-cursor.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/wikimedia-action/token-bag.test.ts +9 -20
- package/node_modules/pi-lean-host/core/auth.ts +2 -1
- package/node_modules/pi-lean-host/core/helpers.ts +29 -35
- package/node_modules/pi-lean-host/core/oauth-command.ts +10 -8
- package/node_modules/pi-lean-host/core/oauth-flow.ts +20 -4
- package/node_modules/pi-lean-host/core/parse-api-guide.ts +3 -5
- package/node_modules/pi-lean-host/core/transport.ts +1 -1
- package/node_modules/pi-lean-host/package.json +1 -1
- package/node_modules/pi-lean-host/tools/api-guide.ts +18 -57
- package/node_modules/pi-lean-host/tools/api-learn.ts +20 -49
- package/node_modules/pi-lean-host/tools/api-probe.ts +17 -26
- package/node_modules/pi-lean-host/tools/api-scaffold.ts +20 -42
- package/node_modules/pi-lean-host/tools/utils.ts +69 -0
- package/node_modules/pi-lean-portal/AGENTS.md +4 -3
- package/node_modules/pi-lean-portal/README.md +9 -9
- package/node_modules/pi-lean-portal/__tests__/accessibility-tree.test.ts +0 -15
- package/node_modules/pi-lean-portal/__tests__/browser-install.test.ts +737 -0
- package/node_modules/pi-lean-portal/__tests__/browser-navigate.test.ts +33 -2
- package/node_modules/pi-lean-portal/__tests__/browser-status.test.ts +48 -25
- package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -9
- package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +3 -29
- package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +51 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-pi.ts +34 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +14 -24
- package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +0 -90
- package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +32 -125
- package/node_modules/pi-lean-portal/__tests__/router-dispatch.test.ts +12 -170
- package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +39 -151
- package/node_modules/pi-lean-portal/__tests__/web-guides.test.ts +1 -44
- package/node_modules/pi-lean-portal/backends/chromium/index.ts +1 -1
- package/node_modules/pi-lean-portal/backends/firefox/index.ts +1 -1
- package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +1 -1
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +43 -57
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/transport.py +6 -21
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_transport.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +157 -73
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_transport.py +15 -41
- package/node_modules/pi-lean-portal/browser-install.ts +454 -0
- package/node_modules/pi-lean-portal/browser-status.ts +21 -0
- package/node_modules/pi-lean-portal/browser-toggle.ts +13 -11
- package/node_modules/pi-lean-portal/core/fetch-backend.ts +2 -2
- package/node_modules/pi-lean-portal/core/router.ts +7 -32
- package/node_modules/pi-lean-portal/core/shared/session-manager.ts +1 -3
- package/node_modules/pi-lean-portal/index.ts +1 -18
- package/node_modules/pi-lean-portal/package.json +2 -1
- package/node_modules/pi-lean-portal/tools/browser-navigate.ts +6 -2
- package/node_modules/pi-lean-portal/tools/utils.ts +5 -6
- package/node_modules/pi-lean-search/AGENTS.md +5 -3
- package/node_modules/pi-lean-search/__tests__/web-search.test.ts +180 -127
- package/node_modules/pi-lean-search/index.ts +0 -3
- package/node_modules/pi-lean-search/package.json +1 -1
- package/node_modules/pi-lean-search/web-search-tool.ts +22 -13
- package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/package.json +1 -1
- package/package.json +4 -4
|
@@ -788,7 +788,7 @@ async function wizardFields(
|
|
|
788
788
|
GRANT_ITEMS,
|
|
789
789
|
);
|
|
790
790
|
if (grant === undefined) {
|
|
791
|
-
|
|
791
|
+
cancelled();
|
|
792
792
|
return undefined;
|
|
793
793
|
}
|
|
794
794
|
let redirectUri: string | undefined;
|
|
@@ -805,11 +805,12 @@ async function wizardFields(
|
|
|
805
805
|
)
|
|
806
806
|
)?.trim();
|
|
807
807
|
if (raw === undefined) {
|
|
808
|
-
|
|
808
|
+
cancelled();
|
|
809
809
|
return undefined;
|
|
810
810
|
}
|
|
811
811
|
if (raw !== "") redirectUri = raw;
|
|
812
812
|
}
|
|
813
|
+
// ast-grep-ignore: hardcoded-url
|
|
813
814
|
const tokenUrl = (
|
|
814
815
|
await ctx.ui.input(
|
|
815
816
|
`Token endpoint URL for '${storeDomain}'`,
|
|
@@ -817,7 +818,7 @@ async function wizardFields(
|
|
|
817
818
|
)
|
|
818
819
|
)?.trim();
|
|
819
820
|
if (!tokenUrl) {
|
|
820
|
-
|
|
821
|
+
cancelled();
|
|
821
822
|
return undefined;
|
|
822
823
|
}
|
|
823
824
|
// Store-name rule: client-id/secret are picked from provisioned secrets —
|
|
@@ -836,7 +837,7 @@ async function wizardFields(
|
|
|
836
837
|
names,
|
|
837
838
|
);
|
|
838
839
|
if (clientId === undefined) {
|
|
839
|
-
|
|
840
|
+
cancelled();
|
|
840
841
|
return undefined;
|
|
841
842
|
}
|
|
842
843
|
const clientSecret = await ctx.ui.select(
|
|
@@ -844,7 +845,7 @@ async function wizardFields(
|
|
|
844
845
|
grant === "client_credentials" ? names : [OMIT_SECRET, ...names],
|
|
845
846
|
);
|
|
846
847
|
if (clientSecret === undefined) {
|
|
847
|
-
|
|
848
|
+
cancelled();
|
|
848
849
|
return undefined;
|
|
849
850
|
}
|
|
850
851
|
// A public PKCE client (secret omitted) sends no client credentials —
|
|
@@ -860,7 +861,7 @@ async function wizardFields(
|
|
|
860
861
|
AUTH_METHOD_ITEMS,
|
|
861
862
|
)) as OAuth2TokenEndpointAuthMethod | undefined);
|
|
862
863
|
if (tokenEndpointAuthMethod === undefined) {
|
|
863
|
-
|
|
864
|
+
cancelled();
|
|
864
865
|
return undefined;
|
|
865
866
|
}
|
|
866
867
|
if (grant === "client_credentials") {
|
|
@@ -874,6 +875,7 @@ async function wizardFields(
|
|
|
874
875
|
},
|
|
875
876
|
};
|
|
876
877
|
}
|
|
878
|
+
// ast-grep-ignore: hardcoded-url
|
|
877
879
|
const authorizeUrl = (
|
|
878
880
|
await ctx.ui.input(
|
|
879
881
|
`Authorization endpoint URL for '${storeDomain}'`,
|
|
@@ -881,7 +883,7 @@ async function wizardFields(
|
|
|
881
883
|
)
|
|
882
884
|
)?.trim();
|
|
883
885
|
if (!authorizeUrl) {
|
|
884
|
-
|
|
886
|
+
cancelled();
|
|
885
887
|
return undefined;
|
|
886
888
|
}
|
|
887
889
|
const scopesRaw = await ctx.ui.input(
|
|
@@ -889,7 +891,7 @@ async function wizardFields(
|
|
|
889
891
|
"e.g. read,profile — empty to skip",
|
|
890
892
|
);
|
|
891
893
|
if (scopesRaw === undefined) {
|
|
892
|
-
|
|
894
|
+
cancelled();
|
|
893
895
|
return undefined;
|
|
894
896
|
}
|
|
895
897
|
const scopes = scopesRaw
|
|
@@ -316,20 +316,36 @@ async function startAuthCodeFlow(
|
|
|
316
316
|
// wrap) so they never have to hunt for it in the scrollback above.
|
|
317
317
|
// Retry loop: a bad paste (typo, state mismatch, exchange hiccup) just
|
|
318
318
|
// re-prompts — the pending flow (verifier + state) is unchanged, so a
|
|
319
|
-
// retry is always safe. Escape
|
|
319
|
+
// retry is always safe. Escape opens the recovery input below.
|
|
320
320
|
for (;;) {
|
|
321
321
|
const pasted = await ctx.ui.input(
|
|
322
322
|
`Open this URL in YOUR browser and authorize, then paste the redirect URL (or just the code) for '${storeDomain}':\n` +
|
|
323
323
|
printableAuthorizeUrl(authorizeUrl),
|
|
324
324
|
"paste the address-bar URL after consenting",
|
|
325
325
|
);
|
|
326
|
-
|
|
327
|
-
|
|
326
|
+
if (pasted === undefined) {
|
|
327
|
+
// Esc at the paste: one recovery input instead of a hard cancel.
|
|
328
|
+
// Empty Enter (or a re-typed identical URI — a prefilling client
|
|
329
|
+
// submits the default on plain Enter) → paste again; the pending flow
|
|
330
|
+
// was never touched, so resuming is free and any code already issued
|
|
331
|
+
// against this authorize URL stays valid. A different typed URI →
|
|
332
|
+
// restart the flow with it (fresh PKCE/state/authorize URL, wizard
|
|
333
|
+
// answers intact since the mint call never returned). Esc → the
|
|
334
|
+
// --code nudge, pending survives.
|
|
335
|
+
const recovery = await ctx.ui.input(
|
|
336
|
+
`Redirect URI — Enter to keep ${redirectUri} and try the paste again, or type a corrected URI (Esc to abort)`,
|
|
337
|
+
redirectUri,
|
|
338
|
+
);
|
|
339
|
+
if (recovery === undefined) break;
|
|
340
|
+
const typed = recovery.trim();
|
|
341
|
+
if (typed === "" || typed === redirectUri) continue;
|
|
342
|
+
return startAuthCodeFlow(auth, storeDomain, ctx, typed);
|
|
343
|
+
}
|
|
328
344
|
try {
|
|
329
345
|
return await completePastedCode(auth, storeDomain, pasted);
|
|
330
346
|
} catch (err) {
|
|
331
347
|
ctx.ui.notify(
|
|
332
|
-
`🔑 ${err instanceof Error ? err.message : String(err)} — paste again, or escape
|
|
348
|
+
`🔑 ${err instanceof Error ? err.message : String(err)} — paste again, or escape for recovery options.`,
|
|
333
349
|
"warning",
|
|
334
350
|
);
|
|
335
351
|
}
|
|
@@ -780,9 +780,8 @@ function validateResponseShape(
|
|
|
780
780
|
|
|
781
781
|
/**
|
|
782
782
|
* Validate a `Record<string, string>` auth sub-field (headers, secretRefs,
|
|
783
|
-
* secretQueryRefs
|
|
784
|
-
*
|
|
785
|
-
* (default: any string; `headerPrefixes` passes a non-empty check).
|
|
783
|
+
* secretQueryRefs). Returns the parsed record, or a ParseError when `raw` is
|
|
784
|
+
* absent/null/non-object/an array or any value is not a string.
|
|
786
785
|
*/
|
|
787
786
|
function parseStringRecord(
|
|
788
787
|
raw: unknown,
|
|
@@ -790,13 +789,12 @@ function parseStringRecord(
|
|
|
790
789
|
fm: string,
|
|
791
790
|
field: string,
|
|
792
791
|
expect: string,
|
|
793
|
-
valueOk: (v: unknown) => boolean = (v) => typeof v === "string",
|
|
794
792
|
): Record<string, string> | ParseApiGuideResult {
|
|
795
793
|
if (
|
|
796
794
|
raw === null ||
|
|
797
795
|
typeof raw !== "object" ||
|
|
798
796
|
Array.isArray(raw) ||
|
|
799
|
-
Object.values(raw).some((v) =>
|
|
797
|
+
Object.values(raw).some((v) => typeof v !== "string")
|
|
800
798
|
) {
|
|
801
799
|
return fail(file, field, expect, describeFound(raw), {
|
|
802
800
|
snippet: snippetFor(fm, "auth"),
|
|
@@ -231,7 +231,7 @@ function cacheKey(url: string, opts?: FetchOptions): string {
|
|
|
231
231
|
* normalized by this). Defined here so the transport layer — the one place
|
|
232
232
|
* that holds a raw request URL and may embed it in an error message — can
|
|
233
233
|
* self-redact instead of relying on callers to remember. `helpers.ts`
|
|
234
|
-
*
|
|
234
|
+
* uses it for the capture points it owns (result.url, urls[],
|
|
235
235
|
* HelperError.url).
|
|
236
236
|
*/
|
|
237
237
|
export function redactSecretParams(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lean-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Read-only REST API client for Pi. Your agent authors and calls YAML recipes handling the tedious parts: pagination, OAuth2, rate limits, backoff. Credentials live in a local store, injected in code, never in agent context. Churn-free research + retrieval",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -13,18 +13,18 @@ import {
|
|
|
13
13
|
} from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { Type } from "@earendil-works/pi-ai";
|
|
15
15
|
import { Text } from "@earendil-works/pi-tui";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
appendFooter,
|
|
18
|
+
disambiguationMenuResult,
|
|
19
|
+
shortNameErrorResult,
|
|
20
|
+
} from "./utils.js";
|
|
17
21
|
import {
|
|
18
22
|
loadAllGuides,
|
|
19
23
|
findGuidesByDomain,
|
|
20
24
|
getCatalogText,
|
|
21
25
|
} from "../core/guide-store.js";
|
|
22
26
|
import { TODAY } from "../core/parse-api-guide.js";
|
|
23
|
-
import {
|
|
24
|
-
formatGuideListings,
|
|
25
|
-
selectGuideByShortName,
|
|
26
|
-
shortNameErrorText,
|
|
27
|
-
} from "../core/guide-catalog.js";
|
|
27
|
+
import { selectGuideByShortName } from "../core/guide-catalog.js";
|
|
28
28
|
import { authStatusLine, canonicalStoreDomain } from "../core/auth.js";
|
|
29
29
|
import { INLINE_LIMIT } from "../core/response-spill.js";
|
|
30
30
|
import type { ApiGuide } from "../core/api-guide-types.js";
|
|
@@ -90,28 +90,12 @@ export const apiGuideTool = defineTool({
|
|
|
90
90
|
if (guideSelector) {
|
|
91
91
|
const sel = selectGuideByShortName(matches, guideSelector);
|
|
92
92
|
if (!sel.ok) {
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
domain,
|
|
100
|
-
guideSelector,
|
|
101
|
-
`Call api-guide({domain: "${domain}"}) to see the menu.`,
|
|
102
|
-
),
|
|
103
|
-
},
|
|
104
|
-
],
|
|
105
|
-
details:
|
|
106
|
-
sel.reason === "no_match"
|
|
107
|
-
? { error: "no_guide_by_shortname", domain, guide: guideSelector }
|
|
108
|
-
: {
|
|
109
|
-
error: "ambiguous_shortname",
|
|
110
|
-
domain,
|
|
111
|
-
guide: guideSelector,
|
|
112
|
-
directories: sel.directories,
|
|
113
|
-
},
|
|
114
|
-
};
|
|
93
|
+
return shortNameErrorResult(
|
|
94
|
+
sel,
|
|
95
|
+
domain,
|
|
96
|
+
guideSelector,
|
|
97
|
+
`Call api-guide({domain: "${domain}"}) to see the menu.`,
|
|
98
|
+
);
|
|
115
99
|
}
|
|
116
100
|
return renderGuideDetail(sel.guide, domain);
|
|
117
101
|
}
|
|
@@ -121,7 +105,12 @@ export const apiGuideTool = defineTool({
|
|
|
121
105
|
}
|
|
122
106
|
|
|
123
107
|
// Multiple guides for one domain → disambiguation menu.
|
|
124
|
-
return
|
|
108
|
+
return disambiguationMenuResult(
|
|
109
|
+
domain,
|
|
110
|
+
matches,
|
|
111
|
+
(shortName) =>
|
|
112
|
+
`Call api-guide({domain: "${domain}", guide: "${shortName}"}) for details.`,
|
|
113
|
+
);
|
|
125
114
|
},
|
|
126
115
|
|
|
127
116
|
renderCall(args, theme, _context) {
|
|
@@ -292,31 +281,3 @@ function renderGuideDetail(
|
|
|
292
281
|
},
|
|
293
282
|
};
|
|
294
283
|
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Disambiguation menu for a domain claimed by more than one guide. Lists
|
|
298
|
-
* each guide's shortName (+ description when present) and a truncated op-name
|
|
299
|
-
* summary; the org header shows only when all matches share one organization.
|
|
300
|
-
*/
|
|
301
|
-
function renderDisambiguationMenu(
|
|
302
|
-
domain: string,
|
|
303
|
-
matches: { guide: ApiGuide; dirName: string }[],
|
|
304
|
-
): AgentToolResult<unknown> {
|
|
305
|
-
const orgs = new Set(
|
|
306
|
-
matches.map((m) => m.guide.organization).filter((o): o is string => !!o),
|
|
307
|
-
);
|
|
308
|
-
const orgName = [...orgs][0];
|
|
309
|
-
const orgPart =
|
|
310
|
-
orgs.size === 1 && orgName ? ` (organization: ${orgName})` : "";
|
|
311
|
-
const lines: string[] = [];
|
|
312
|
-
lines.push(`${matches.length} API guides for '${domain}'${orgPart}:`);
|
|
313
|
-
lines.push(formatGuideListings(matches));
|
|
314
|
-
const example = matches[0]!.guide.shortName;
|
|
315
|
-
lines.push(
|
|
316
|
-
`Call api-guide({domain: "${domain}", guide: "${example}"}) for details.`,
|
|
317
|
-
);
|
|
318
|
-
return {
|
|
319
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
320
|
-
details: { domain, disambiguation: matches.length },
|
|
321
|
-
};
|
|
322
|
-
}
|
|
@@ -27,7 +27,12 @@ import {
|
|
|
27
27
|
} from "@earendil-works/pi-coding-agent";
|
|
28
28
|
import { Type } from "@earendil-works/pi-ai";
|
|
29
29
|
import { Text } from "@earendil-works/pi-tui";
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
appendFooter,
|
|
32
|
+
contentText,
|
|
33
|
+
disambiguationMenuResult,
|
|
34
|
+
shortNameErrorResult,
|
|
35
|
+
} from "./utils.js";
|
|
31
36
|
import {
|
|
32
37
|
existsSync,
|
|
33
38
|
mkdirSync,
|
|
@@ -43,11 +48,7 @@ import {
|
|
|
43
48
|
parseApiGuide,
|
|
44
49
|
stampFrontmatterField,
|
|
45
50
|
} from "../core/parse-api-guide.js";
|
|
46
|
-
import {
|
|
47
|
-
formatGuideListings,
|
|
48
|
-
selectGuideByShortName,
|
|
49
|
-
shortNameErrorText,
|
|
50
|
-
} from "../core/guide-catalog.js";
|
|
51
|
+
import { selectGuideByShortName } from "../core/guide-catalog.js";
|
|
51
52
|
import {
|
|
52
53
|
GUIDE_SCHEMA_VERSION,
|
|
53
54
|
type ApiGuide,
|
|
@@ -415,25 +416,6 @@ function stageFetchedRecipe(
|
|
|
415
416
|
};
|
|
416
417
|
}
|
|
417
418
|
|
|
418
|
-
/** Disambiguation menu for the N-guide fetch-recipe case (mirrors
|
|
419
|
-
* api-guide's menu). */
|
|
420
|
-
function renderFetchMenu(
|
|
421
|
-
domain: string,
|
|
422
|
-
matches: { guide: ApiGuide; dirName: string }[],
|
|
423
|
-
): AgentToolResult<unknown> {
|
|
424
|
-
const lines: string[] = [];
|
|
425
|
-
lines.push(`${matches.length} API guides for '${domain}':`);
|
|
426
|
-
lines.push(formatGuideListings(matches));
|
|
427
|
-
const example = matches[0]!.guide.shortName;
|
|
428
|
-
lines.push(
|
|
429
|
-
`Call api-learn({domain: "${domain}", guide: "${example}"}) to fetch one guide's recipe.`,
|
|
430
|
-
);
|
|
431
|
-
return {
|
|
432
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
433
|
-
details: { mode: "menu", domain, disambiguation: matches.length },
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
|
|
437
419
|
// ═══════════════════════════════════════════════════════════════════
|
|
438
420
|
// Tool definition
|
|
439
421
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -541,34 +523,23 @@ export const apiLearnTool = defineTool({
|
|
|
541
523
|
const { guide, dirName } = matches[0]!;
|
|
542
524
|
return stageFetchedRecipe(domain, guide, dirName);
|
|
543
525
|
}
|
|
544
|
-
// N guides → disambiguation by shortName (
|
|
526
|
+
// N guides → disambiguation by shortName (shared menu helper).
|
|
545
527
|
if (!guideSelector) {
|
|
546
|
-
return
|
|
528
|
+
return disambiguationMenuResult(
|
|
529
|
+
domain,
|
|
530
|
+
matches,
|
|
531
|
+
(shortName) =>
|
|
532
|
+
`Call api-learn({domain: "${domain}", guide: "${shortName}"}) to fetch one guide's recipe.`,
|
|
533
|
+
);
|
|
547
534
|
}
|
|
548
535
|
const sel = selectGuideByShortName(matches, guideSelector);
|
|
549
536
|
if (!sel.ok) {
|
|
550
|
-
return
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
domain,
|
|
557
|
-
guideSelector,
|
|
558
|
-
`Call api-learn({domain: "${domain}"}) to see the menu.`,
|
|
559
|
-
),
|
|
560
|
-
},
|
|
561
|
-
],
|
|
562
|
-
details:
|
|
563
|
-
sel.reason === "no_match"
|
|
564
|
-
? { error: "no_guide_by_shortname", domain, guide: guideSelector }
|
|
565
|
-
: {
|
|
566
|
-
error: "ambiguous_shortname",
|
|
567
|
-
domain,
|
|
568
|
-
guide: guideSelector,
|
|
569
|
-
directories: sel.directories,
|
|
570
|
-
},
|
|
571
|
-
};
|
|
537
|
+
return shortNameErrorResult(
|
|
538
|
+
sel,
|
|
539
|
+
domain,
|
|
540
|
+
guideSelector,
|
|
541
|
+
`Call api-learn({domain: "${domain}"}) to see the menu.`,
|
|
542
|
+
);
|
|
572
543
|
}
|
|
573
544
|
return stageFetchedRecipe(domain, sel.guide, sel.dirName);
|
|
574
545
|
}
|
|
@@ -93,8 +93,6 @@ export interface ProbeResult {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
export interface ProbeOptions {
|
|
96
|
-
/** Accept header (default application/json). */
|
|
97
|
-
accept?: string;
|
|
98
96
|
/** On 404, walk the apiHost version backward (vN→v1). Default true. */
|
|
99
97
|
walkVersions?: boolean;
|
|
100
98
|
/**
|
|
@@ -563,7 +561,6 @@ export async function probe(
|
|
|
563
561
|
opts: ProbeOptions = {},
|
|
564
562
|
ctx?: ExtensionContext,
|
|
565
563
|
): Promise<ProbeResult> {
|
|
566
|
-
const accept = opts.accept ?? "application/json";
|
|
567
564
|
const walkVersions = opts.walkVersions ?? true;
|
|
568
565
|
const domain =
|
|
569
566
|
opts.domain ?? resolveProvisionedParentDomain(hostnameOf(apiHost));
|
|
@@ -575,7 +572,6 @@ export async function probe(
|
|
|
575
572
|
apiHost,
|
|
576
573
|
path,
|
|
577
574
|
params,
|
|
578
|
-
accept,
|
|
579
575
|
authCtx,
|
|
580
576
|
domain,
|
|
581
577
|
versionPrefixOf(apiHost),
|
|
@@ -593,7 +589,7 @@ export async function probe(
|
|
|
593
589
|
return base;
|
|
594
590
|
}
|
|
595
591
|
const hit = await walkBackward(
|
|
596
|
-
{ apiHost, path, params,
|
|
592
|
+
{ apiHost, path, params, authCtx, domain },
|
|
597
593
|
Number(stated),
|
|
598
594
|
);
|
|
599
595
|
return hit ?? base;
|
|
@@ -615,7 +611,6 @@ async function walkBackward(
|
|
|
615
611
|
apiHost: string;
|
|
616
612
|
path: string;
|
|
617
613
|
params: Record<string, unknown>;
|
|
618
|
-
accept: string;
|
|
619
614
|
authCtx: ProbeAuthCtx;
|
|
620
615
|
domain: string;
|
|
621
616
|
},
|
|
@@ -628,7 +623,6 @@ async function walkBackward(
|
|
|
628
623
|
withVersion(ctx.apiHost, k),
|
|
629
624
|
ctx.path,
|
|
630
625
|
ctx.params,
|
|
631
|
-
ctx.accept,
|
|
632
626
|
ctx.authCtx,
|
|
633
627
|
ctx.domain,
|
|
634
628
|
`/v${k}`,
|
|
@@ -667,7 +661,6 @@ async function fetchOne(
|
|
|
667
661
|
apiHost: string,
|
|
668
662
|
path: string,
|
|
669
663
|
params: Record<string, unknown>,
|
|
670
|
-
accept: string,
|
|
671
664
|
authCtx: ProbeAuthCtx,
|
|
672
665
|
domain: string,
|
|
673
666
|
prefix = "",
|
|
@@ -694,7 +687,7 @@ async function fetchOne(
|
|
|
694
687
|
const url = redactUrl(rawUrl);
|
|
695
688
|
const hasQuerySecret = Object.keys(authCtx.queryParams).length > 0;
|
|
696
689
|
const res = await fetchUrl(rawUrl, {
|
|
697
|
-
headers: { accept, ...authCtx.headers },
|
|
690
|
+
headers: { accept: "application/json", ...authCtx.headers },
|
|
698
691
|
fresh: true,
|
|
699
692
|
...(authCtx.hasAuthBlock
|
|
700
693
|
? {
|
|
@@ -787,8 +780,7 @@ async function fetchOne(
|
|
|
787
780
|
shape: null,
|
|
788
781
|
draft: "",
|
|
789
782
|
raw,
|
|
790
|
-
note:
|
|
791
|
-
"non-JSON body (set opts.accept for XML/HTML, or use a different path)",
|
|
783
|
+
note: "non-JSON body (use a different path)",
|
|
792
784
|
};
|
|
793
785
|
}
|
|
794
786
|
|
|
@@ -839,21 +831,20 @@ function buildUrl(
|
|
|
839
831
|
const substituted = fillPathTemplate(path, fillParams);
|
|
840
832
|
const qs = new URLSearchParams(
|
|
841
833
|
Object.fromEntries(
|
|
842
|
-
Object.entries(queryParams)
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
}),
|
|
834
|
+
Object.entries(queryParams).flatMap(([k, v]): [string, string][] => {
|
|
835
|
+
if (v === undefined || pathTokens.has(k)) return [];
|
|
836
|
+
// Probe has no guide schema to declare listStyle against, so
|
|
837
|
+
// non-scalar values are a loud error — never the silent
|
|
838
|
+
// String(v) wire form (String(["a","b"]) → "a,b" is
|
|
839
|
+
// accidentally comma-correct but misdescribes repeat/bracket
|
|
840
|
+
// APIs; [object Object] is wrong everywhere).
|
|
841
|
+
if (Array.isArray(v) || (typeof v === "object" && v !== null)) {
|
|
842
|
+
throw new Error(
|
|
843
|
+
`Param "${k}" is ${Array.isArray(v) ? "an array" : "an object"} — api-probe sends scalar params only: pre-join list values as a string, or JSON.stringify DSL params`,
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
return [[k, String(v)]];
|
|
847
|
+
}),
|
|
857
848
|
),
|
|
858
849
|
).toString();
|
|
859
850
|
return joinUrl(apiHost, substituted, qs);
|
|
@@ -27,14 +27,15 @@
|
|
|
27
27
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
28
28
|
import { Type } from "@earendil-works/pi-ai";
|
|
29
29
|
import { Text } from "@earendil-works/pi-tui";
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
appendFooter,
|
|
32
|
+
contentText,
|
|
33
|
+
disambiguationMenuResult,
|
|
34
|
+
shortNameErrorResult,
|
|
35
|
+
} from "./utils.js";
|
|
31
36
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
32
37
|
import { join } from "node:path";
|
|
33
|
-
import {
|
|
34
|
-
formatGuideListings,
|
|
35
|
-
selectGuideByShortName,
|
|
36
|
-
shortNameErrorText,
|
|
37
|
-
} from "../core/guide-catalog.js";
|
|
38
|
+
import { selectGuideByShortName } from "../core/guide-catalog.js";
|
|
38
39
|
import type { ApiGuide } from "../core/api-guide-types.js";
|
|
39
40
|
import { findGuidesByDomain, getUserGuidesDir } from "../core/guide-store.js";
|
|
40
41
|
import { assertSafeDomain, slug } from "../core/path-template.js";
|
|
@@ -235,45 +236,22 @@ export const apiScaffoldTool = defineTool({
|
|
|
235
236
|
} else if (guideSelector) {
|
|
236
237
|
const sel = selectGuideByShortName(matches, guideSelector);
|
|
237
238
|
if (!sel.ok) {
|
|
238
|
-
return
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
domain,
|
|
245
|
-
guideSelector,
|
|
246
|
-
`Call api-scaffold({domain: "${domain}", verify: true}) to see the menu.`,
|
|
247
|
-
),
|
|
248
|
-
},
|
|
249
|
-
],
|
|
250
|
-
details:
|
|
251
|
-
sel.reason === "no_match"
|
|
252
|
-
? { error: "no_guide_by_shortname", domain, guide: guideSelector }
|
|
253
|
-
: {
|
|
254
|
-
error: "ambiguous_shortname",
|
|
255
|
-
domain,
|
|
256
|
-
guide: guideSelector,
|
|
257
|
-
directories: sel.directories,
|
|
258
|
-
},
|
|
259
|
-
};
|
|
239
|
+
return shortNameErrorResult(
|
|
240
|
+
sel,
|
|
241
|
+
domain,
|
|
242
|
+
guideSelector,
|
|
243
|
+
`Call api-scaffold({domain: "${domain}", verify: true}) to see the menu.`,
|
|
244
|
+
);
|
|
260
245
|
}
|
|
261
246
|
selected = sel;
|
|
262
247
|
} else {
|
|
263
|
-
// N guides, no selector → disambiguation menu
|
|
264
|
-
return
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
formatGuideListings(matches),
|
|
271
|
-
`Call api-scaffold({domain: "${domain}", guide: "${matches[0]!.guide.shortName}", verify: true}) to scaffold one.`,
|
|
272
|
-
].join("\n"),
|
|
273
|
-
},
|
|
274
|
-
],
|
|
275
|
-
details: { mode: "menu", domain, disambiguation: matches.length },
|
|
276
|
-
};
|
|
248
|
+
// N guides, no selector → disambiguation menu.
|
|
249
|
+
return disambiguationMenuResult(
|
|
250
|
+
domain,
|
|
251
|
+
matches,
|
|
252
|
+
(shortName) =>
|
|
253
|
+
`Call api-scaffold({domain: "${domain}", guide: "${shortName}", verify: true}) to scaffold one.`,
|
|
254
|
+
);
|
|
277
255
|
}
|
|
278
256
|
|
|
279
257
|
const { guide, dirName } = selected;
|
|
@@ -2,6 +2,12 @@ import type {
|
|
|
2
2
|
AgentToolResult,
|
|
3
3
|
ThemeColor,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
formatGuideListings,
|
|
7
|
+
selectGuideByShortName,
|
|
8
|
+
shortNameErrorText,
|
|
9
|
+
} from "../core/guide-catalog.js";
|
|
10
|
+
import type { ApiGuide } from "../core/api-guide-types.js";
|
|
5
11
|
|
|
6
12
|
/**
|
|
7
13
|
* Extract the text of the first text content block of a tool result.
|
|
@@ -15,6 +21,69 @@ export function contentText(
|
|
|
15
21
|
return c && c.type === "text" ? c.text : fallback;
|
|
16
22
|
}
|
|
17
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Tool-channel error result for a failed `selectGuideByShortName` — the
|
|
26
|
+
* shared envelope (error text + structured details) behind api-guide,
|
|
27
|
+
* api-learn, and api-scaffold's `{domain, guide}` resolution. The trailing
|
|
28
|
+
* "how to see the menu" hint differs per tool, so callers pass it.
|
|
29
|
+
*/
|
|
30
|
+
export function shortNameErrorResult(
|
|
31
|
+
sel: Extract<ReturnType<typeof selectGuideByShortName>, { ok: false }>,
|
|
32
|
+
domain: string,
|
|
33
|
+
selector: string,
|
|
34
|
+
callToAction: string,
|
|
35
|
+
): AgentToolResult<unknown> {
|
|
36
|
+
return {
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: "text",
|
|
40
|
+
text: shortNameErrorText(sel, domain, selector, callToAction),
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
details:
|
|
44
|
+
sel.reason === "no_match"
|
|
45
|
+
? { error: "no_guide_by_shortname", domain, guide: selector }
|
|
46
|
+
: {
|
|
47
|
+
error: "ambiguous_shortname",
|
|
48
|
+
domain,
|
|
49
|
+
guide: selector,
|
|
50
|
+
directories: sel.directories,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Tool-channel disambiguation menu for a domain claimed by N guides:
|
|
57
|
+
* count header (+ shared `organization:` when all matches share one),
|
|
58
|
+
* per-guide listings, and a per-tool call-to-action footer built from
|
|
59
|
+
* the first match's shortName.
|
|
60
|
+
*/
|
|
61
|
+
export function disambiguationMenuResult(
|
|
62
|
+
domain: string,
|
|
63
|
+
matches: { guide: ApiGuide }[],
|
|
64
|
+
callToAction: (shortName: string) => string,
|
|
65
|
+
): AgentToolResult<unknown> {
|
|
66
|
+
const orgs = new Set(
|
|
67
|
+
matches.map((m) => m.guide.organization).filter((o): o is string => !!o),
|
|
68
|
+
);
|
|
69
|
+
const orgName = [...orgs][0];
|
|
70
|
+
const orgPart =
|
|
71
|
+
orgs.size === 1 && orgName ? ` (organization: ${orgName})` : "";
|
|
72
|
+
return {
|
|
73
|
+
content: [
|
|
74
|
+
{
|
|
75
|
+
type: "text",
|
|
76
|
+
text: [
|
|
77
|
+
`${matches.length} API guides for '${domain}'${orgPart}:`,
|
|
78
|
+
formatGuideListings(matches),
|
|
79
|
+
callToAction(matches[0]!.guide.shortName),
|
|
80
|
+
].join("\n"),
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
details: { mode: "menu", domain, disambiguation: matches.length },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
18
87
|
/**
|
|
19
88
|
* Append a dim-styled content preview to an in-progress result string,
|
|
20
89
|
* with a "more chars" suffix when the content exceeds the given limit.
|