ultimate-pi 0.19.1 → 0.22.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/.agents/skills/harness-decisions/SKILL.md +68 -2
- package/.agents/skills/harness-git-commit/SKILL.md +72 -0
- package/.agents/skills/harness-governor/SKILL.md +2 -2
- package/.agents/skills/harness-ls-lint-setup/SKILL.md +59 -0
- package/.agents/skills/harness-plan/SKILL.md +13 -11
- package/.agents/skills/harness-review/SKILL.md +1 -1
- package/.agents/skills/harness-sentrux-repair/SKILL.md +48 -0
- package/.agents/skills/sentrux/SKILL.md +4 -2
- package/.agents/skills/wiki-save/SKILL.md +1 -1
- package/.pi/PACKAGING.md +6 -0
- package/.pi/SYSTEM.md +21 -3
- package/.pi/agents/harness/ls-lint-steward.md +49 -0
- package/.pi/agents/harness/planning/decompose.md +4 -4
- package/.pi/agents/harness/reviewing/evaluator.md +1 -1
- package/.pi/agents/harness/running/executor.md +43 -2
- package/.pi/agents/harness/sentrux-repair-advisor.md +50 -0
- package/.pi/agents/pi-pi/prompt-expert.md +17 -2
- package/.pi/auto-commit.json +9 -2
- package/.pi/extensions/debate-orchestrator.ts +3 -0
- package/.pi/extensions/harness-anchored-edit.ts +139 -0
- package/.pi/extensions/harness-ask-user.ts +13 -34
- package/.pi/extensions/harness-debate-tools.ts +43 -4
- package/.pi/extensions/harness-live-widget.ts +28 -19
- package/.pi/extensions/harness-run-context.ts +278 -115
- package/.pi/extensions/harness-web-tools.ts +598 -471
- package/.pi/extensions/ls-lint-rules-sync.ts +103 -0
- package/.pi/extensions/observation-bus.ts +4 -0
- package/.pi/extensions/policy-gate.ts +270 -229
- package/.pi/extensions/sentrux-rules-sync.ts +2 -0
- package/.pi/extensions/soundboard.ts +48 -48
- package/.pi/harness/README.md +4 -0
- package/.pi/harness/agents.manifest.json +15 -7
- package/.pi/harness/agents.policy.yaml +47 -81
- package/.pi/harness/docs/adrs/0051-hash-anchored-executor-edits.md +41 -0
- package/.pi/harness/docs/adrs/0052-ls-lint-naming-lifecycle.md +45 -0
- package/.pi/harness/docs/adrs/0052-sentrux-structured-repair.md +38 -0
- package/.pi/harness/docs/adrs/0053-plan-task-clarification-gate.md +39 -0
- package/.pi/harness/docs/adrs/0054-harness-native-ask-user.md +40 -0
- package/.pi/harness/docs/adrs/0055-auto-commit-coauthor-lifecycle.md +40 -0
- package/.pi/harness/docs/adrs/README.md +7 -0
- package/.pi/harness/docs/practice-map.md +21 -5
- package/.pi/harness/evals/smoke/ls-lint-stub.json +10 -0
- package/.pi/harness/evolution/self-healing-rules.json +16 -0
- package/.pi/harness/ls-lint/naming.manifest.json +128 -0
- package/.pi/harness/sentrux/architecture.manifest.json +1 -1
- package/.pi/harness/specs/auto-commit.schema.json +63 -0
- package/.pi/harness/specs/ls-lint-manifest-proposal.schema.json +80 -0
- package/.pi/harness/specs/ls-lint-signal.schema.json +47 -0
- package/.pi/harness/specs/naming-manifest.schema.json +54 -0
- package/.pi/harness/specs/plan-task-clarification.schema.json +88 -0
- package/.pi/harness/specs/sentrux-diagnostics.schema.json +173 -0
- package/.pi/harness/specs/sentrux-repair-plan.schema.json +133 -0
- package/.pi/harness/specs/sentrux-report.schema.json +119 -0
- package/.pi/harness/specs/sentrux-signal.schema.json +34 -1
- package/.pi/lib/agents-policy.d.mts +26 -47
- package/.pi/lib/agents-policy.mjs +84 -29
- package/.pi/lib/agents-policy.ts +1 -0
- package/.pi/lib/agt/build-evaluation-context.ts +136 -64
- package/.pi/lib/ask-user/constants.mjs +3 -0
- package/.pi/lib/ask-user/constants.ts +4 -0
- package/.pi/lib/ask-user/contracts/glimpse-parse.ts +56 -0
- package/.pi/lib/ask-user/contracts/glimpse-payload-build.ts +58 -0
- package/.pi/lib/ask-user/contracts/glimpse-payload.ts +38 -0
- package/.pi/lib/ask-user/core/questionnaire.ts +74 -0
- package/.pi/lib/ask-user/dialog.ts +2 -314
- package/.pi/lib/ask-user/fallback.ts +2 -78
- package/.pi/lib/ask-user/format.ts +85 -0
- package/.pi/lib/ask-user/glimpseui.d.ts +10 -0
- package/.pi/lib/ask-user/index.ts +114 -0
- package/.pi/lib/ask-user/merge-task-clarification.ts +98 -0
- package/.pi/lib/ask-user/policy.mjs +43 -0
- package/.pi/lib/ask-user/policy.ts +104 -0
- package/.pi/lib/ask-user/presenters/glimpse.ts +130 -0
- package/.pi/lib/ask-user/presenters/headless.ts +131 -0
- package/.pi/lib/ask-user/presenters/select.ts +60 -0
- package/.pi/lib/ask-user/presenters/tui.ts +373 -0
- package/.pi/lib/ask-user/presenters/types.ts +13 -0
- package/.pi/lib/ask-user/render.ts +40 -9
- package/.pi/lib/ask-user/schema.ts +66 -13
- package/.pi/lib/ask-user/types.ts +60 -3
- package/.pi/lib/ask-user/validate-core.mjs +193 -7
- package/.pi/lib/ask-user/validate.ts +53 -34
- package/.pi/lib/harness-anchored-edit/.hash_anchors +1721 -0
- package/.pi/lib/harness-anchored-edit/anchor-state.ts +320 -0
- package/.pi/lib/harness-anchored-edit/apply-anchored-edits.ts +161 -0
- package/.pi/lib/harness-anchored-edit/edit-executor.ts +146 -0
- package/.pi/lib/harness-anchored-edit/index.ts +9 -0
- package/.pi/lib/harness-anchored-edit/line-protocol.ts +38 -0
- package/.pi/lib/harness-anchored-edit/package.json +3 -0
- package/.pi/lib/harness-anchored-edit/settings.ts +1 -0
- package/.pi/lib/harness-anchored-edit/task-id.ts +8 -0
- package/.pi/lib/harness-anchored-edit/types.ts +19 -0
- package/.pi/lib/harness-artifact-gate.ts +75 -21
- package/.pi/lib/harness-auto-commit-config.mjs +321 -0
- package/.pi/lib/harness-lens/clients/anchored-edit-autopatch.ts +158 -0
- package/.pi/lib/harness-lens/clients/lsp/client.ts +62 -39
- package/.pi/lib/harness-lens/clients/tool-policy.ts +73 -181
- package/.pi/lib/harness-lens/index.ts +246 -96
- package/.pi/lib/harness-lens/tools/lsp-navigation.ts +10 -8
- package/.pi/lib/harness-repair-brief.ts +84 -25
- package/.pi/lib/harness-run-context.ts +42 -52
- package/.pi/lib/harness-sentrux-parse.mjs +272 -0
- package/.pi/lib/harness-sentrux-root.mjs +78 -0
- package/.pi/lib/harness-slash-completions.ts +116 -0
- package/.pi/lib/harness-spawn-topology.ts +121 -87
- package/.pi/lib/harness-subagent-submit-registry.ts +10 -0
- package/.pi/lib/harness-subagents-bridge.ts +11 -6
- package/.pi/lib/harness-ui-state.ts +95 -48
- package/.pi/lib/plan-approval/dialog.ts +5 -0
- package/.pi/lib/plan-approval/validate.ts +1 -1
- package/.pi/lib/plan-approval-readiness.ts +32 -0
- package/.pi/lib/plan-debate-gate.ts +154 -114
- package/.pi/lib/plan-task-clarification.ts +158 -0
- package/.pi/prompts/harness-auto.md +2 -2
- package/.pi/prompts/harness-ls-lint-steward.md +43 -0
- package/.pi/prompts/harness-plan.md +58 -8
- package/.pi/prompts/harness-review.md +40 -6
- package/.pi/prompts/harness-run.md +33 -11
- package/.pi/prompts/harness-setup.md +72 -3
- package/.pi/prompts/harness-steer.md +3 -2
- package/.pi/prompts/wiki-save.md +5 -4
- package/.pi/scripts/README.md +8 -0
- package/.pi/scripts/generate-agents-policy-yaml.mjs +14 -2
- package/.pi/scripts/harness-anchored-edit-smoke.mjs +45 -0
- package/.pi/scripts/harness-auto-commit-bootstrap.mjs +96 -0
- package/.pi/scripts/harness-cli-verify.sh +47 -0
- package/.pi/scripts/harness-git-churn.mjs +77 -0
- package/.pi/scripts/harness-git-commit.mjs +173 -0
- package/.pi/scripts/harness-ls-lint-bootstrap.mjs +142 -0
- package/.pi/scripts/harness-ls-lint-cli.mjs +184 -0
- package/.pi/scripts/harness-seed-project-contracts.mjs +47 -0
- package/.pi/scripts/harness-sentrux-diagnostics.mjs +230 -0
- package/.pi/scripts/harness-sentrux-report.mjs +256 -0
- package/.pi/scripts/harness-verify.mjs +347 -117
- package/.pi/scripts/ls-lint-rules-sync.mjs +265 -0
- package/.pi/scripts/run-tests.mjs +65 -0
- package/.pi/settings.example.json +1 -0
- package/.sentrux/rules.toml +1 -1
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +31 -0
- package/README.md +13 -4
- package/THIRD_PARTY_NOTICES.md +7 -0
- package/package.json +8 -3
- package/vendor/pi-subagents/src/agents.ts +5 -0
- package/vendor/pi-subagents/src/subagents.ts +22 -3
- package/vendor/pi-vcc/src/hooks/before-compact.ts +86 -60
- package/.pi/scripts/release.sh +0 -338
|
@@ -10,19 +10,19 @@ import { claimHarnessGovernanceLoad } from "../lib/extension-load-guard.js";
|
|
|
10
10
|
import {
|
|
11
11
|
rememberSessionWebArtifactDir,
|
|
12
12
|
resolveWebOutputPath,
|
|
13
|
-
webArtifactScopeHint,
|
|
14
13
|
type WebArtifactScope,
|
|
14
|
+
webArtifactScopeHint,
|
|
15
15
|
} from "../lib/harness-web/artifacts.js";
|
|
16
16
|
import {
|
|
17
|
+
type FetchCacheContext,
|
|
17
18
|
fingerprintFile,
|
|
18
19
|
formatCacheAge,
|
|
19
20
|
lookupFetchCache,
|
|
20
21
|
lookupSearchCache,
|
|
21
22
|
publishWorkspaceAlias,
|
|
23
|
+
type SearchCacheContext,
|
|
22
24
|
writeFetchCacheEntry,
|
|
23
25
|
writeSearchCacheEntry,
|
|
24
|
-
type FetchCacheContext,
|
|
25
|
-
type SearchCacheContext,
|
|
26
26
|
} from "../lib/harness-web/cache.js";
|
|
27
27
|
import {
|
|
28
28
|
harnessWebContextLine,
|
|
@@ -32,7 +32,6 @@ import {
|
|
|
32
32
|
summarizeSearchJson,
|
|
33
33
|
} from "../lib/harness-web/run-cli.js";
|
|
34
34
|
|
|
35
|
-
// @ts-expect-error pi extensions run as ESM
|
|
36
35
|
const MODULE_URL = import.meta.url;
|
|
37
36
|
|
|
38
37
|
const WEB_SEARCH_GUIDELINES = [
|
|
@@ -135,7 +134,8 @@ const WebSearchSchema = Type.Object({
|
|
|
135
134
|
),
|
|
136
135
|
limit: Type.Optional(
|
|
137
136
|
Type.Number({
|
|
138
|
-
description:
|
|
137
|
+
description:
|
|
138
|
+
"Max results (tier defaults: instant 5, standard 10, deep 10)",
|
|
139
139
|
minimum: 1,
|
|
140
140
|
maximum: 20,
|
|
141
141
|
}),
|
|
@@ -181,10 +181,14 @@ const WebFetchSchema = Type.Object({
|
|
|
181
181
|
}),
|
|
182
182
|
),
|
|
183
183
|
highlightQuery: Type.Optional(
|
|
184
|
-
Type.String({
|
|
184
|
+
Type.String({
|
|
185
|
+
description: "Query for highlight scoring (required if highlights)",
|
|
186
|
+
}),
|
|
185
187
|
),
|
|
186
188
|
highlightsOutput: Type.Optional(
|
|
187
|
-
Type.String({
|
|
189
|
+
Type.String({
|
|
190
|
+
description: "Highlights JSON path (default .web/highlights.json)",
|
|
191
|
+
}),
|
|
188
192
|
),
|
|
189
193
|
limit: Type.Optional(
|
|
190
194
|
Type.Number({
|
|
@@ -216,7 +220,9 @@ const WebFindSimilarSchema = Type.Object({
|
|
|
216
220
|
const WebContentsSchema = Type.Object({
|
|
217
221
|
webScope: WebScopeSchema,
|
|
218
222
|
urls: Type.Optional(
|
|
219
|
-
Type.Array(Type.String(), {
|
|
223
|
+
Type.Array(Type.String(), {
|
|
224
|
+
description: "URLs to fetch (or use fromSearch)",
|
|
225
|
+
}),
|
|
220
226
|
),
|
|
221
227
|
fromSearch: Type.Optional(
|
|
222
228
|
Type.String({
|
|
@@ -315,6 +321,586 @@ function resolveTier(params: { tier?: string; bulk?: boolean }): string {
|
|
|
315
321
|
return "deep";
|
|
316
322
|
}
|
|
317
323
|
|
|
324
|
+
function executeWebSearchBulk(args: {
|
|
325
|
+
ctx: any;
|
|
326
|
+
cwd: string;
|
|
327
|
+
webScope: string | undefined;
|
|
328
|
+
query: string;
|
|
329
|
+
limit: number | undefined;
|
|
330
|
+
outputParam: unknown;
|
|
331
|
+
}) {
|
|
332
|
+
const bulkScoped = resolveScopedOutput(
|
|
333
|
+
args.ctx,
|
|
334
|
+
"bulk",
|
|
335
|
+
args.outputParam ? `${args.outputParam}` : undefined,
|
|
336
|
+
args.webScope,
|
|
337
|
+
);
|
|
338
|
+
const output = bulkScoped.output.endsWith("/bulk")
|
|
339
|
+
? bulkScoped.output
|
|
340
|
+
: `${bulkScoped.artifactDir}/bulk`;
|
|
341
|
+
ensureParentDir(args.cwd, output);
|
|
342
|
+
const lim = args.limit ?? 3;
|
|
343
|
+
const argv = [
|
|
344
|
+
"bulk-scrape",
|
|
345
|
+
args.query,
|
|
346
|
+
"-o",
|
|
347
|
+
output,
|
|
348
|
+
"--limit",
|
|
349
|
+
String(lim),
|
|
350
|
+
];
|
|
351
|
+
const run = runHarnessWeb(MODULE_URL, argv, args.cwd);
|
|
352
|
+
if (!run.ok) {
|
|
353
|
+
return failResult(
|
|
354
|
+
`web_search bulk failed (exit ${run.exitCode}).\n${run.stderr || run.stdout}`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
return okResult(
|
|
358
|
+
`${run.stdout}\n\noutput: ${output}\nartifactDir: ${bulkScoped.artifactDir}`,
|
|
359
|
+
{
|
|
360
|
+
output,
|
|
361
|
+
artifactDir: bulkScoped.artifactDir,
|
|
362
|
+
query: args.query,
|
|
363
|
+
bulk: true,
|
|
364
|
+
},
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function resolveAnglesFile(args: {
|
|
369
|
+
params: any;
|
|
370
|
+
ctx: any;
|
|
371
|
+
cwd: string;
|
|
372
|
+
query: string;
|
|
373
|
+
webScope: string | undefined;
|
|
374
|
+
}): string {
|
|
375
|
+
let anglesFile = String(args.params.anglesFile ?? "").trim();
|
|
376
|
+
if (anglesFile && !anglesFile.startsWith("/") && !anglesFile.includes("..")) {
|
|
377
|
+
anglesFile = resolveScopedOutput(
|
|
378
|
+
args.ctx,
|
|
379
|
+
"angles.yaml",
|
|
380
|
+
anglesFile,
|
|
381
|
+
args.webScope,
|
|
382
|
+
).output;
|
|
383
|
+
}
|
|
384
|
+
if (args.params.angles?.length && !anglesFile) {
|
|
385
|
+
const inline = resolveScopedOutput(
|
|
386
|
+
args.ctx,
|
|
387
|
+
"angles-inline.yaml",
|
|
388
|
+
undefined,
|
|
389
|
+
args.webScope,
|
|
390
|
+
);
|
|
391
|
+
const tmp = resolve(args.cwd, inline.output);
|
|
392
|
+
ensureParentDir(args.cwd, inline.output);
|
|
393
|
+
const yaml =
|
|
394
|
+
`intent: ${JSON.stringify(args.query)}\nangles:\n` +
|
|
395
|
+
args.params.angles
|
|
396
|
+
.map(
|
|
397
|
+
(q: string, i: number) =>
|
|
398
|
+
` - id: angle_${i + 1}\n query: ${JSON.stringify(q)}`,
|
|
399
|
+
)
|
|
400
|
+
.join("\n") +
|
|
401
|
+
"\n";
|
|
402
|
+
writeFileSync(tmp, yaml, "utf-8");
|
|
403
|
+
anglesFile = inline.output;
|
|
404
|
+
}
|
|
405
|
+
return anglesFile;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function tryWebSearchCacheHit(args: {
|
|
409
|
+
refreshCache: boolean;
|
|
410
|
+
cwd: string;
|
|
411
|
+
searchCtx: SearchCacheContext;
|
|
412
|
+
maxAgeSec?: number;
|
|
413
|
+
basename: string;
|
|
414
|
+
scopedArtifactDir: string;
|
|
415
|
+
tier: string;
|
|
416
|
+
query: string;
|
|
417
|
+
engine: string;
|
|
418
|
+
}): ReturnType<typeof okResult> | null {
|
|
419
|
+
if (args.refreshCache) return null;
|
|
420
|
+
const cached = lookupSearchCache(args.cwd, args.searchCtx, {
|
|
421
|
+
maxAgeSec: args.maxAgeSec,
|
|
422
|
+
});
|
|
423
|
+
if (!(cached.hit && !cached.stale)) return null;
|
|
424
|
+
const workspaceOutput = publishWorkspaceAlias(
|
|
425
|
+
args.cwd,
|
|
426
|
+
cached.artifactPath,
|
|
427
|
+
args.basename,
|
|
428
|
+
);
|
|
429
|
+
const parts = [
|
|
430
|
+
`[cache hit] age ${formatCacheAge(cached.ageMs)} · key ${cached.cacheKey}`,
|
|
431
|
+
`cache: ${cached.entryDir}`,
|
|
432
|
+
];
|
|
433
|
+
const summary =
|
|
434
|
+
args.tier === "deep" || args.tier === "research"
|
|
435
|
+
? summarizeDeepSearchJson(workspaceOutput, args.cwd)
|
|
436
|
+
: summarizeSearchJson(workspaceOutput, args.cwd);
|
|
437
|
+
if (summary) parts.push("", summary);
|
|
438
|
+
parts.push(
|
|
439
|
+
"",
|
|
440
|
+
`output: ${workspaceOutput}`,
|
|
441
|
+
`artifactDir: ${args.scopedArtifactDir}`,
|
|
442
|
+
`tier: ${args.tier}`,
|
|
443
|
+
);
|
|
444
|
+
parts.push("Read output JSON; web_fetch top URLs with highlights:true.");
|
|
445
|
+
return okResult(parts.join("\n"), {
|
|
446
|
+
output: workspaceOutput,
|
|
447
|
+
artifactDir: args.scopedArtifactDir,
|
|
448
|
+
query: args.query,
|
|
449
|
+
tier: args.tier,
|
|
450
|
+
engine: args.engine,
|
|
451
|
+
cacheHit: true,
|
|
452
|
+
cacheKey: cached.cacheKey,
|
|
453
|
+
cachePath: cached.artifactPath,
|
|
454
|
+
cacheAgeMs: cached.ageMs,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function buildWebSearchArgv(args: {
|
|
459
|
+
tier: string;
|
|
460
|
+
query: string;
|
|
461
|
+
output: string;
|
|
462
|
+
resultLimit: number;
|
|
463
|
+
anglesFile: string;
|
|
464
|
+
expandHeuristic: boolean;
|
|
465
|
+
category?: string;
|
|
466
|
+
limit?: number;
|
|
467
|
+
}): string[] {
|
|
468
|
+
if (args.tier === "deep" || args.tier === "research") {
|
|
469
|
+
const argv = [
|
|
470
|
+
"search-deep",
|
|
471
|
+
args.query,
|
|
472
|
+
"-o",
|
|
473
|
+
args.output,
|
|
474
|
+
"--limit",
|
|
475
|
+
String(args.resultLimit),
|
|
476
|
+
];
|
|
477
|
+
if (args.anglesFile) {
|
|
478
|
+
argv.push("--angles-file", args.anglesFile);
|
|
479
|
+
} else if (args.expandHeuristic) {
|
|
480
|
+
argv.push("--expand-heuristic");
|
|
481
|
+
}
|
|
482
|
+
if (args.category) argv.push("--category", args.category);
|
|
483
|
+
return argv;
|
|
484
|
+
}
|
|
485
|
+
return [
|
|
486
|
+
"search",
|
|
487
|
+
args.query,
|
|
488
|
+
"-o",
|
|
489
|
+
args.output,
|
|
490
|
+
"--tier",
|
|
491
|
+
args.tier,
|
|
492
|
+
...(args.limit != null ? ["--limit", String(args.limit)] : []),
|
|
493
|
+
];
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function executeWebSearch(params: any, ctx: any) {
|
|
497
|
+
const cwd = sessionCwd(ctx);
|
|
498
|
+
const webScope = String(params.webScope ?? "").trim() || undefined;
|
|
499
|
+
const query = String(params.query ?? "").trim();
|
|
500
|
+
if (!query) return failResult("web_search: query is required.");
|
|
501
|
+
|
|
502
|
+
const tier = resolveTier(params);
|
|
503
|
+
const bulk = params.bulk === true;
|
|
504
|
+
const limit = typeof params.limit === "number" ? params.limit : undefined;
|
|
505
|
+
|
|
506
|
+
if (bulk) {
|
|
507
|
+
return executeWebSearchBulk({
|
|
508
|
+
ctx,
|
|
509
|
+
cwd,
|
|
510
|
+
webScope,
|
|
511
|
+
query,
|
|
512
|
+
limit,
|
|
513
|
+
outputParam: params.output,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const basename =
|
|
518
|
+
tier === "deep" || tier === "research" ? "search-deep.json" : "search.json";
|
|
519
|
+
const scoped = resolveScopedOutput(
|
|
520
|
+
ctx,
|
|
521
|
+
basename,
|
|
522
|
+
params.output ? String(params.output) : undefined,
|
|
523
|
+
webScope,
|
|
524
|
+
);
|
|
525
|
+
const output = scoped.output;
|
|
526
|
+
ensureParentDir(cwd, output);
|
|
527
|
+
const { refresh: refreshCache, maxAgeSec } = cacheControlFromParams(params);
|
|
528
|
+
const engine = searchEngineId();
|
|
529
|
+
const resultLimit = limit ?? 10;
|
|
530
|
+
const category = params.category ? String(params.category) : undefined;
|
|
531
|
+
|
|
532
|
+
const anglesFile = resolveAnglesFile({
|
|
533
|
+
params,
|
|
534
|
+
ctx,
|
|
535
|
+
cwd,
|
|
536
|
+
query,
|
|
537
|
+
webScope,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
if (
|
|
541
|
+
(tier === "deep" || tier === "research") &&
|
|
542
|
+
!anglesFile &&
|
|
543
|
+
params.expandHeuristic !== true &&
|
|
544
|
+
!params.angles?.length
|
|
545
|
+
) {
|
|
546
|
+
return failResult(
|
|
547
|
+
"web_search tier=deep requires anglesFile (.web/angles.yaml from harness/web-retrieval/web-query-expander) " +
|
|
548
|
+
"or expandHeuristic:true. Invoke web-retrieval skill first.",
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const anglesFingerprint = anglesFile
|
|
553
|
+
? fingerprintFile(cwd, anglesFile)
|
|
554
|
+
: undefined;
|
|
555
|
+
|
|
556
|
+
const searchCtx: SearchCacheContext = {
|
|
557
|
+
query,
|
|
558
|
+
tier,
|
|
559
|
+
engine,
|
|
560
|
+
limit: resultLimit,
|
|
561
|
+
category,
|
|
562
|
+
expandHeuristic: params.expandHeuristic === true,
|
|
563
|
+
anglesFingerprint,
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const cacheHit = tryWebSearchCacheHit({
|
|
567
|
+
refreshCache,
|
|
568
|
+
cwd,
|
|
569
|
+
searchCtx,
|
|
570
|
+
maxAgeSec,
|
|
571
|
+
basename,
|
|
572
|
+
scopedArtifactDir: scoped.artifactDir,
|
|
573
|
+
tier,
|
|
574
|
+
query,
|
|
575
|
+
engine,
|
|
576
|
+
});
|
|
577
|
+
if (cacheHit) return cacheHit;
|
|
578
|
+
|
|
579
|
+
const argv = buildWebSearchArgv({
|
|
580
|
+
tier,
|
|
581
|
+
query,
|
|
582
|
+
output,
|
|
583
|
+
resultLimit,
|
|
584
|
+
anglesFile,
|
|
585
|
+
expandHeuristic: params.expandHeuristic === true,
|
|
586
|
+
category,
|
|
587
|
+
limit,
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
const run = runHarnessWeb(MODULE_URL, argv, cwd);
|
|
591
|
+
if (!run.ok) {
|
|
592
|
+
const hint =
|
|
593
|
+
"\n\nHints: run /harness-setup; for searxng set HARNESS_WEB_SEARXNG_URL; " +
|
|
594
|
+
"enable json in SearXNG search.formats; for deep spawn web-query-expander first.";
|
|
595
|
+
return failResult(
|
|
596
|
+
`web_search failed (exit ${run.exitCode}).\n${run.stderr || run.stdout}${hint}`,
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const cacheWrite = writeSearchCacheEntry(cwd, searchCtx, output, {
|
|
601
|
+
anglesPath: anglesFile,
|
|
602
|
+
});
|
|
603
|
+
publishWorkspaceAlias(cwd, `${cacheWrite.entryDir}/${basename}`, basename);
|
|
604
|
+
|
|
605
|
+
const parts = [run.stdout];
|
|
606
|
+
const summary =
|
|
607
|
+
tier === "deep" || tier === "research"
|
|
608
|
+
? summarizeDeepSearchJson(output, cwd)
|
|
609
|
+
: summarizeSearchJson(output, cwd);
|
|
610
|
+
if (summary) parts.push("", summary);
|
|
611
|
+
parts.push(
|
|
612
|
+
"",
|
|
613
|
+
`output: ${output}`,
|
|
614
|
+
`artifactDir: ${scoped.artifactDir}`,
|
|
615
|
+
`tier: ${tier}`,
|
|
616
|
+
`cache: ${cacheWrite.entryDir}`,
|
|
617
|
+
);
|
|
618
|
+
parts.push("Read output JSON; web_fetch top URLs with highlights:true.");
|
|
619
|
+
|
|
620
|
+
return okResult(parts.join("\n"), {
|
|
621
|
+
output,
|
|
622
|
+
artifactDir: scoped.artifactDir,
|
|
623
|
+
query,
|
|
624
|
+
tier,
|
|
625
|
+
engine,
|
|
626
|
+
cacheHit: false,
|
|
627
|
+
cacheKey: cacheWrite.cacheKey,
|
|
628
|
+
cachePath: `${cacheWrite.entryDir}/${basename}`,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function executeWebFetch(params: any, ctx: any) {
|
|
633
|
+
const cwd = sessionCwd(ctx);
|
|
634
|
+
const webScope = String(params.webScope ?? "").trim() || undefined;
|
|
635
|
+
const url = String(params.url ?? "").trim();
|
|
636
|
+
if (!url) return failResult("web_fetch: url is required.");
|
|
637
|
+
|
|
638
|
+
const mode = params.mode === "map" ? "map" : "scrape";
|
|
639
|
+
const fast = params.fast === true;
|
|
640
|
+
const limit = typeof params.limit === "number" ? params.limit : 100;
|
|
641
|
+
const basename = mode === "map" ? "map.json" : "page.md";
|
|
642
|
+
const scoped = resolveScopedOutput(
|
|
643
|
+
ctx,
|
|
644
|
+
basename,
|
|
645
|
+
params.output ? String(params.output) : undefined,
|
|
646
|
+
webScope,
|
|
647
|
+
);
|
|
648
|
+
const output = scoped.output;
|
|
649
|
+
ensureParentDir(cwd, output);
|
|
650
|
+
const highlights = params.highlights === true;
|
|
651
|
+
const hlQuery = String(params.highlightQuery ?? "").trim();
|
|
652
|
+
const { refresh: refreshCache, maxAgeSec } = cacheControlFromParams(params);
|
|
653
|
+
|
|
654
|
+
const hlScoped =
|
|
655
|
+
highlights && !params.highlightsOutput
|
|
656
|
+
? resolveScopedOutput(ctx, "highlights.json", undefined, webScope)
|
|
657
|
+
: highlights
|
|
658
|
+
? resolveScopedOutput(
|
|
659
|
+
ctx,
|
|
660
|
+
"highlights.json",
|
|
661
|
+
String(params.highlightsOutput),
|
|
662
|
+
webScope,
|
|
663
|
+
)
|
|
664
|
+
: undefined;
|
|
665
|
+
if (hlScoped) ensureParentDir(cwd, hlScoped.output);
|
|
666
|
+
|
|
667
|
+
const fetchCtx: FetchCacheContext = {
|
|
668
|
+
url,
|
|
669
|
+
mode,
|
|
670
|
+
fast,
|
|
671
|
+
highlightQuery: hlQuery || undefined,
|
|
672
|
+
highlights,
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
if (!refreshCache) {
|
|
676
|
+
const cached = lookupFetchCache(cwd, fetchCtx, { maxAgeSec });
|
|
677
|
+
if (cached.hit && !cached.stale) {
|
|
678
|
+
const workspaceBasename = highlights
|
|
679
|
+
? "highlights.json"
|
|
680
|
+
: mode === "map"
|
|
681
|
+
? "map.json"
|
|
682
|
+
: "page.md";
|
|
683
|
+
const workspaceOutput = publishWorkspaceAlias(
|
|
684
|
+
cwd,
|
|
685
|
+
cached.artifactPath,
|
|
686
|
+
workspaceBasename,
|
|
687
|
+
);
|
|
688
|
+
const parts = [
|
|
689
|
+
`[cache hit] age ${formatCacheAge(cached.ageMs)} · key ${cached.cacheKey}`,
|
|
690
|
+
`cache: ${cached.entryDir}`,
|
|
691
|
+
"",
|
|
692
|
+
`output: ${workspaceOutput}`,
|
|
693
|
+
`artifactDir: ${scoped.artifactDir}`,
|
|
694
|
+
];
|
|
695
|
+
const excerpt = readTextExcerpt(workspaceOutput, cwd);
|
|
696
|
+
if (excerpt) parts.push("", "--- excerpt ---", excerpt);
|
|
697
|
+
return okResult(parts.join("\n"), {
|
|
698
|
+
output: workspaceOutput,
|
|
699
|
+
artifactDir: scoped.artifactDir,
|
|
700
|
+
url,
|
|
701
|
+
mode,
|
|
702
|
+
highlights,
|
|
703
|
+
cacheHit: true,
|
|
704
|
+
cacheKey: cached.cacheKey,
|
|
705
|
+
cachePath: cached.artifactPath,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
let argv: string[];
|
|
711
|
+
if (mode === "map") {
|
|
712
|
+
argv = [
|
|
713
|
+
"map",
|
|
714
|
+
url,
|
|
715
|
+
"-o",
|
|
716
|
+
output,
|
|
717
|
+
"--limit",
|
|
718
|
+
String(limit),
|
|
719
|
+
...(fast ? ["--fast"] : []),
|
|
720
|
+
];
|
|
721
|
+
} else {
|
|
722
|
+
argv = ["scrape", url, "-o", output, ...(fast ? ["--fast"] : [])];
|
|
723
|
+
if (highlights) {
|
|
724
|
+
if (!hlQuery) {
|
|
725
|
+
return failResult(
|
|
726
|
+
"web_fetch: highlightQuery required when highlights=true",
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
argv.push("--highlights", "--highlight-query", hlQuery);
|
|
730
|
+
if (hlScoped) argv.push("--highlights-output", hlScoped.output);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const run = runHarnessWeb(MODULE_URL, argv, cwd);
|
|
735
|
+
if (!run.ok) {
|
|
736
|
+
return failResult(
|
|
737
|
+
`web_fetch failed (exit ${run.exitCode}).\n${run.stderr || run.stdout}\n` +
|
|
738
|
+
"Try fast:true for static pages, or run harness-cli-verify for Scrapling install.",
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const cacheArtifact = highlights && hlScoped ? hlScoped.output : output;
|
|
743
|
+
const cacheWrite = writeFetchCacheEntry(cwd, fetchCtx, cacheArtifact, {
|
|
744
|
+
highlightsPath:
|
|
745
|
+
highlights && hlScoped && hlScoped.output !== cacheArtifact
|
|
746
|
+
? hlScoped.output
|
|
747
|
+
: undefined,
|
|
748
|
+
});
|
|
749
|
+
const workspaceBasename = highlights
|
|
750
|
+
? "highlights.json"
|
|
751
|
+
: mode === "map"
|
|
752
|
+
? "map.json"
|
|
753
|
+
: "page.md";
|
|
754
|
+
publishWorkspaceAlias(
|
|
755
|
+
cwd,
|
|
756
|
+
`${cacheWrite.entryDir}/${workspaceBasename}`,
|
|
757
|
+
workspaceBasename,
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
const parts = [
|
|
761
|
+
run.stdout,
|
|
762
|
+
"",
|
|
763
|
+
`output: ${output}`,
|
|
764
|
+
`artifactDir: ${scoped.artifactDir}`,
|
|
765
|
+
`cache: ${cacheWrite.entryDir}`,
|
|
766
|
+
];
|
|
767
|
+
const excerpt = readTextExcerpt(output, cwd);
|
|
768
|
+
if (excerpt) parts.push("", "--- excerpt ---", excerpt);
|
|
769
|
+
|
|
770
|
+
return okResult(parts.join("\n"), {
|
|
771
|
+
output,
|
|
772
|
+
artifactDir: scoped.artifactDir,
|
|
773
|
+
url,
|
|
774
|
+
mode,
|
|
775
|
+
highlights,
|
|
776
|
+
cacheHit: false,
|
|
777
|
+
cacheKey: cacheWrite.cacheKey,
|
|
778
|
+
cachePath: `${cacheWrite.entryDir}/${workspaceBasename}`,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async function executeWebFindSimilar(params: any, ctx: any) {
|
|
783
|
+
const cwd = sessionCwd(ctx);
|
|
784
|
+
const webScope = String(params.webScope ?? "").trim() || undefined;
|
|
785
|
+
const url = String(params.url ?? "").trim();
|
|
786
|
+
if (!url) return failResult("web_find_similar: url is required.");
|
|
787
|
+
|
|
788
|
+
const scoped = resolveScopedOutput(
|
|
789
|
+
ctx,
|
|
790
|
+
"search-deep.json",
|
|
791
|
+
params.output ? String(params.output) : undefined,
|
|
792
|
+
webScope,
|
|
793
|
+
);
|
|
794
|
+
const output = scoped.output;
|
|
795
|
+
ensureParentDir(cwd, output);
|
|
796
|
+
const limit = typeof params.limit === "number" ? params.limit : 10;
|
|
797
|
+
const argv = [
|
|
798
|
+
"find-similar",
|
|
799
|
+
url,
|
|
800
|
+
"-o",
|
|
801
|
+
output,
|
|
802
|
+
"--limit",
|
|
803
|
+
String(limit),
|
|
804
|
+
...(params.fast !== false ? ["--fast"] : []),
|
|
805
|
+
];
|
|
806
|
+
|
|
807
|
+
const run = runHarnessWeb(MODULE_URL, argv, cwd);
|
|
808
|
+
if (!run.ok) {
|
|
809
|
+
return failResult(
|
|
810
|
+
`web_find_similar failed (exit ${run.exitCode}).\n${run.stderr || run.stdout}`,
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const parts = [run.stdout];
|
|
815
|
+
const summary = summarizeDeepSearchJson(output, cwd);
|
|
816
|
+
if (summary) parts.push("", summary);
|
|
817
|
+
parts.push("", `output: ${output}`, `artifactDir: ${scoped.artifactDir}`);
|
|
818
|
+
|
|
819
|
+
return okResult(parts.join("\n"), {
|
|
820
|
+
output,
|
|
821
|
+
artifactDir: scoped.artifactDir,
|
|
822
|
+
url,
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
async function executeWebContents(params: any, ctx: any) {
|
|
827
|
+
const cwd = sessionCwd(ctx);
|
|
828
|
+
const webScope = String(params.webScope ?? "").trim() || undefined;
|
|
829
|
+
const dirScoped = resolveScopedOutput(
|
|
830
|
+
ctx,
|
|
831
|
+
"contents",
|
|
832
|
+
params.outputDir ? String(params.outputDir) : undefined,
|
|
833
|
+
webScope,
|
|
834
|
+
);
|
|
835
|
+
const outputDir = dirScoped.output.endsWith("/contents")
|
|
836
|
+
? dirScoped.output
|
|
837
|
+
: `${dirScoped.artifactDir}/contents`;
|
|
838
|
+
mkdirSync(resolve(cwd, outputDir), { recursive: true });
|
|
839
|
+
let fromSearch = String(params.fromSearch ?? "").trim();
|
|
840
|
+
if (fromSearch && !fromSearch.startsWith("/") && !fromSearch.includes("..")) {
|
|
841
|
+
fromSearch = resolveScopedOutput(
|
|
842
|
+
ctx,
|
|
843
|
+
"search-deep.json",
|
|
844
|
+
fromSearch,
|
|
845
|
+
webScope,
|
|
846
|
+
).output;
|
|
847
|
+
}
|
|
848
|
+
const urls = (params.urls ?? [])
|
|
849
|
+
.map((u: unknown) => String(u).trim())
|
|
850
|
+
.filter(Boolean);
|
|
851
|
+
const limit = typeof params.limit === "number" ? params.limit : 5;
|
|
852
|
+
const hlQuery = String(params.highlightQuery ?? "").trim();
|
|
853
|
+
|
|
854
|
+
const argv = [
|
|
855
|
+
"contents-batch",
|
|
856
|
+
"-o",
|
|
857
|
+
outputDir,
|
|
858
|
+
"--limit",
|
|
859
|
+
String(limit),
|
|
860
|
+
...(params.fast ? ["--fast"] : []),
|
|
861
|
+
...(params.highlights && hlQuery
|
|
862
|
+
? ["--highlights", "--highlight-query", hlQuery]
|
|
863
|
+
: []),
|
|
864
|
+
...urls,
|
|
865
|
+
];
|
|
866
|
+
if (fromSearch) {
|
|
867
|
+
argv.splice(1, 0, "--from-search", fromSearch);
|
|
868
|
+
}
|
|
869
|
+
let evidencePath: string | undefined;
|
|
870
|
+
if (params.evidenceBundle && fromSearch) {
|
|
871
|
+
const bundleArg = String(params.evidenceBundle);
|
|
872
|
+
evidencePath =
|
|
873
|
+
bundleArg.startsWith("/") || bundleArg.includes("..")
|
|
874
|
+
? bundleArg
|
|
875
|
+
: resolveScopedOutput(ctx, "evidence-bundle.json", bundleArg, webScope)
|
|
876
|
+
.output;
|
|
877
|
+
ensureParentDir(cwd, evidencePath);
|
|
878
|
+
argv.push("--evidence-bundle", evidencePath);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (!fromSearch && !urls.length) {
|
|
882
|
+
return failResult("web_contents: provide urls or fromSearch");
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const run = runHarnessWeb(MODULE_URL, argv, cwd);
|
|
886
|
+
if (!run.ok) {
|
|
887
|
+
return failResult(
|
|
888
|
+
`web_contents failed (exit ${run.exitCode}).\n${run.stderr || run.stdout}`,
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return okResult(
|
|
893
|
+
`${run.stdout}\n\noutputDir: ${outputDir}\nartifactDir: ${dirScoped.artifactDir}` +
|
|
894
|
+
(evidencePath ? `\nevidence: ${evidencePath}` : ""),
|
|
895
|
+
{
|
|
896
|
+
outputDir,
|
|
897
|
+
artifactDir: dirScoped.artifactDir,
|
|
898
|
+
fromSearch,
|
|
899
|
+
evidenceBundle: evidencePath,
|
|
900
|
+
},
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
|
|
318
904
|
export default function harnessWebTools(pi: ExtensionAPI) {
|
|
319
905
|
if (!claimHarnessGovernanceLoad("harness-web-tools", MODULE_URL)) return;
|
|
320
906
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
@@ -342,210 +928,7 @@ export default function harnessWebTools(pi: ExtensionAPI) {
|
|
|
342
928
|
parameters: WebSearchSchema,
|
|
343
929
|
|
|
344
930
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
345
|
-
|
|
346
|
-
const webScope = String(params.webScope ?? "").trim() || undefined;
|
|
347
|
-
const query = String(params.query ?? "").trim();
|
|
348
|
-
if (!query) return failResult("web_search: query is required.");
|
|
349
|
-
|
|
350
|
-
const tier = resolveTier(params);
|
|
351
|
-
const bulk = params.bulk === true;
|
|
352
|
-
const limit = typeof params.limit === "number" ? params.limit : undefined;
|
|
353
|
-
|
|
354
|
-
if (bulk) {
|
|
355
|
-
const bulkScoped = resolveScopedOutput(
|
|
356
|
-
ctx,
|
|
357
|
-
"bulk",
|
|
358
|
-
params.output ? `${params.output}` : undefined,
|
|
359
|
-
webScope,
|
|
360
|
-
);
|
|
361
|
-
const output = bulkScoped.output.endsWith("/bulk")
|
|
362
|
-
? bulkScoped.output
|
|
363
|
-
: `${bulkScoped.artifactDir}/bulk`;
|
|
364
|
-
ensureParentDir(cwd, output);
|
|
365
|
-
const lim = limit ?? 3;
|
|
366
|
-
const argv = ["bulk-scrape", query, "-o", output, "--limit", String(lim)];
|
|
367
|
-
const run = runHarnessWeb(MODULE_URL, argv, cwd);
|
|
368
|
-
if (!run.ok) {
|
|
369
|
-
return failResult(
|
|
370
|
-
`web_search bulk failed (exit ${run.exitCode}).\n${run.stderr || run.stdout}`,
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
return okResult(
|
|
374
|
-
`${run.stdout}\n\noutput: ${output}\nartifactDir: ${bulkScoped.artifactDir}`,
|
|
375
|
-
{ output, artifactDir: bulkScoped.artifactDir, query, bulk: true },
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const basename =
|
|
380
|
-
tier === "deep" || tier === "research" ? "search-deep.json" : "search.json";
|
|
381
|
-
const scoped = resolveScopedOutput(
|
|
382
|
-
ctx,
|
|
383
|
-
basename,
|
|
384
|
-
params.output ? String(params.output) : undefined,
|
|
385
|
-
webScope,
|
|
386
|
-
);
|
|
387
|
-
const output = scoped.output;
|
|
388
|
-
ensureParentDir(cwd, output);
|
|
389
|
-
const { refresh: refreshCache, maxAgeSec } = cacheControlFromParams(params);
|
|
390
|
-
const engine = searchEngineId();
|
|
391
|
-
const resultLimit = limit ?? 10;
|
|
392
|
-
const category = params.category ? String(params.category) : undefined;
|
|
393
|
-
|
|
394
|
-
let anglesFile = String(params.anglesFile ?? "").trim();
|
|
395
|
-
if (anglesFile && !anglesFile.startsWith("/") && !anglesFile.includes("..")) {
|
|
396
|
-
anglesFile = resolveScopedOutput(ctx, "angles.yaml", anglesFile, webScope).output;
|
|
397
|
-
}
|
|
398
|
-
if (params.angles?.length && !anglesFile) {
|
|
399
|
-
const inline = resolveScopedOutput(ctx, "angles-inline.yaml", undefined, webScope);
|
|
400
|
-
const tmp = resolve(cwd, inline.output);
|
|
401
|
-
ensureParentDir(cwd, inline.output);
|
|
402
|
-
const yaml =
|
|
403
|
-
`intent: ${JSON.stringify(query)}\nangles:\n` +
|
|
404
|
-
params.angles
|
|
405
|
-
.map(
|
|
406
|
-
(q, i) =>
|
|
407
|
-
` - id: angle_${i + 1}\n query: ${JSON.stringify(q)}`,
|
|
408
|
-
)
|
|
409
|
-
.join("\n") +
|
|
410
|
-
"\n";
|
|
411
|
-
writeFileSync(tmp, yaml, "utf-8");
|
|
412
|
-
anglesFile = inline.output;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
if (
|
|
416
|
-
(tier === "deep" || tier === "research") &&
|
|
417
|
-
!anglesFile &&
|
|
418
|
-
params.expandHeuristic !== true &&
|
|
419
|
-
!params.angles?.length
|
|
420
|
-
) {
|
|
421
|
-
return failResult(
|
|
422
|
-
"web_search tier=deep requires anglesFile (.web/angles.yaml from harness/web-retrieval/web-query-expander) " +
|
|
423
|
-
"or expandHeuristic:true. Invoke web-retrieval skill first.",
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const anglesFingerprint = anglesFile
|
|
428
|
-
? fingerprintFile(cwd, anglesFile)
|
|
429
|
-
: undefined;
|
|
430
|
-
|
|
431
|
-
const searchCtx: SearchCacheContext = {
|
|
432
|
-
query,
|
|
433
|
-
tier,
|
|
434
|
-
engine,
|
|
435
|
-
limit: resultLimit,
|
|
436
|
-
category,
|
|
437
|
-
expandHeuristic: params.expandHeuristic === true,
|
|
438
|
-
anglesFingerprint,
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
if (!refreshCache) {
|
|
442
|
-
const cached = lookupSearchCache(cwd, searchCtx, { maxAgeSec });
|
|
443
|
-
if (cached.hit && !cached.stale) {
|
|
444
|
-
const workspaceOutput = publishWorkspaceAlias(
|
|
445
|
-
cwd,
|
|
446
|
-
cached.artifactPath,
|
|
447
|
-
basename,
|
|
448
|
-
);
|
|
449
|
-
const parts = [
|
|
450
|
-
`[cache hit] age ${formatCacheAge(cached.ageMs)} · key ${cached.cacheKey}`,
|
|
451
|
-
`cache: ${cached.entryDir}`,
|
|
452
|
-
];
|
|
453
|
-
const summary =
|
|
454
|
-
tier === "deep" || tier === "research"
|
|
455
|
-
? summarizeDeepSearchJson(workspaceOutput, cwd)
|
|
456
|
-
: summarizeSearchJson(workspaceOutput, cwd);
|
|
457
|
-
if (summary) parts.push("", summary);
|
|
458
|
-
parts.push(
|
|
459
|
-
"",
|
|
460
|
-
`output: ${workspaceOutput}`,
|
|
461
|
-
`artifactDir: ${scoped.artifactDir}`,
|
|
462
|
-
`tier: ${tier}`,
|
|
463
|
-
);
|
|
464
|
-
parts.push("Read output JSON; web_fetch top URLs with highlights:true.");
|
|
465
|
-
return okResult(parts.join("\n"), {
|
|
466
|
-
output: workspaceOutput,
|
|
467
|
-
artifactDir: scoped.artifactDir,
|
|
468
|
-
query,
|
|
469
|
-
tier,
|
|
470
|
-
engine,
|
|
471
|
-
cacheHit: true,
|
|
472
|
-
cacheKey: cached.cacheKey,
|
|
473
|
-
cachePath: cached.artifactPath,
|
|
474
|
-
cacheAgeMs: cached.ageMs,
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
let argv: string[];
|
|
480
|
-
if (tier === "deep" || tier === "research") {
|
|
481
|
-
argv = [
|
|
482
|
-
"search-deep",
|
|
483
|
-
query,
|
|
484
|
-
"-o",
|
|
485
|
-
output,
|
|
486
|
-
"--limit",
|
|
487
|
-
String(resultLimit),
|
|
488
|
-
];
|
|
489
|
-
if (anglesFile) {
|
|
490
|
-
argv.push("--angles-file", anglesFile);
|
|
491
|
-
} else if (params.expandHeuristic === true) {
|
|
492
|
-
argv.push("--expand-heuristic");
|
|
493
|
-
}
|
|
494
|
-
if (category) {
|
|
495
|
-
argv.push("--category", category);
|
|
496
|
-
}
|
|
497
|
-
} else {
|
|
498
|
-
argv = [
|
|
499
|
-
"search",
|
|
500
|
-
query,
|
|
501
|
-
"-o",
|
|
502
|
-
output,
|
|
503
|
-
"--tier",
|
|
504
|
-
tier,
|
|
505
|
-
...(limit != null ? ["--limit", String(limit)] : []),
|
|
506
|
-
];
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const run = runHarnessWeb(MODULE_URL, argv, cwd);
|
|
510
|
-
if (!run.ok) {
|
|
511
|
-
const hint =
|
|
512
|
-
"\n\nHints: run /harness-setup; for searxng set HARNESS_WEB_SEARXNG_URL; " +
|
|
513
|
-
"enable json in SearXNG search.formats; for deep spawn web-query-expander first.";
|
|
514
|
-
return failResult(
|
|
515
|
-
`web_search failed (exit ${run.exitCode}).\n${run.stderr || run.stdout}${hint}`,
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const cacheWrite = writeSearchCacheEntry(cwd, searchCtx, output, {
|
|
520
|
-
anglesPath: anglesFile,
|
|
521
|
-
});
|
|
522
|
-
publishWorkspaceAlias(cwd, `${cacheWrite.entryDir}/${basename}`, basename);
|
|
523
|
-
|
|
524
|
-
const parts = [run.stdout];
|
|
525
|
-
const summary =
|
|
526
|
-
tier === "deep" || tier === "research"
|
|
527
|
-
? summarizeDeepSearchJson(output, cwd)
|
|
528
|
-
: summarizeSearchJson(output, cwd);
|
|
529
|
-
if (summary) parts.push("", summary);
|
|
530
|
-
parts.push(
|
|
531
|
-
"",
|
|
532
|
-
`output: ${output}`,
|
|
533
|
-
`artifactDir: ${scoped.artifactDir}`,
|
|
534
|
-
`tier: ${tier}`,
|
|
535
|
-
`cache: ${cacheWrite.entryDir}`,
|
|
536
|
-
);
|
|
537
|
-
parts.push("Read output JSON; web_fetch top URLs with highlights:true.");
|
|
538
|
-
|
|
539
|
-
return okResult(parts.join("\n"), {
|
|
540
|
-
output,
|
|
541
|
-
artifactDir: scoped.artifactDir,
|
|
542
|
-
query,
|
|
543
|
-
tier,
|
|
544
|
-
engine,
|
|
545
|
-
cacheHit: false,
|
|
546
|
-
cacheKey: cacheWrite.cacheKey,
|
|
547
|
-
cachePath: `${cacheWrite.entryDir}/${basename}`,
|
|
548
|
-
});
|
|
931
|
+
return executeWebSearch(params as Record<string, unknown>, ctx);
|
|
549
932
|
},
|
|
550
933
|
});
|
|
551
934
|
|
|
@@ -559,147 +942,7 @@ export default function harnessWebTools(pi: ExtensionAPI) {
|
|
|
559
942
|
parameters: WebFetchSchema,
|
|
560
943
|
|
|
561
944
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
562
|
-
|
|
563
|
-
const webScope = String(params.webScope ?? "").trim() || undefined;
|
|
564
|
-
const url = String(params.url ?? "").trim();
|
|
565
|
-
if (!url) return failResult("web_fetch: url is required.");
|
|
566
|
-
|
|
567
|
-
const mode = params.mode === "map" ? "map" : "scrape";
|
|
568
|
-
const fast = params.fast === true;
|
|
569
|
-
const limit = typeof params.limit === "number" ? params.limit : 100;
|
|
570
|
-
const basename = mode === "map" ? "map.json" : "page.md";
|
|
571
|
-
const scoped = resolveScopedOutput(
|
|
572
|
-
ctx,
|
|
573
|
-
basename,
|
|
574
|
-
params.output ? String(params.output) : undefined,
|
|
575
|
-
webScope,
|
|
576
|
-
);
|
|
577
|
-
const output = scoped.output;
|
|
578
|
-
ensureParentDir(cwd, output);
|
|
579
|
-
const highlights = params.highlights === true;
|
|
580
|
-
const hlQuery = String(params.highlightQuery ?? "").trim();
|
|
581
|
-
const { refresh: refreshCache, maxAgeSec } = cacheControlFromParams(params);
|
|
582
|
-
|
|
583
|
-
const hlScoped =
|
|
584
|
-
highlights && !params.highlightsOutput
|
|
585
|
-
? resolveScopedOutput(ctx, "highlights.json", undefined, webScope)
|
|
586
|
-
: highlights
|
|
587
|
-
? resolveScopedOutput(
|
|
588
|
-
ctx,
|
|
589
|
-
"highlights.json",
|
|
590
|
-
String(params.highlightsOutput),
|
|
591
|
-
webScope,
|
|
592
|
-
)
|
|
593
|
-
: undefined;
|
|
594
|
-
if (hlScoped) ensureParentDir(cwd, hlScoped.output);
|
|
595
|
-
|
|
596
|
-
const fetchCtx: FetchCacheContext = {
|
|
597
|
-
url,
|
|
598
|
-
mode,
|
|
599
|
-
fast,
|
|
600
|
-
highlightQuery: hlQuery || undefined,
|
|
601
|
-
highlights,
|
|
602
|
-
};
|
|
603
|
-
|
|
604
|
-
if (!refreshCache) {
|
|
605
|
-
const cached = lookupFetchCache(cwd, fetchCtx, { maxAgeSec });
|
|
606
|
-
if (cached.hit && !cached.stale) {
|
|
607
|
-
const workspaceBasename = highlights
|
|
608
|
-
? "highlights.json"
|
|
609
|
-
: mode === "map"
|
|
610
|
-
? "map.json"
|
|
611
|
-
: "page.md";
|
|
612
|
-
const workspaceOutput = publishWorkspaceAlias(
|
|
613
|
-
cwd,
|
|
614
|
-
cached.artifactPath,
|
|
615
|
-
workspaceBasename,
|
|
616
|
-
);
|
|
617
|
-
const parts = [
|
|
618
|
-
`[cache hit] age ${formatCacheAge(cached.ageMs)} · key ${cached.cacheKey}`,
|
|
619
|
-
`cache: ${cached.entryDir}`,
|
|
620
|
-
"",
|
|
621
|
-
`output: ${workspaceOutput}`,
|
|
622
|
-
`artifactDir: ${scoped.artifactDir}`,
|
|
623
|
-
];
|
|
624
|
-
const excerpt = readTextExcerpt(workspaceOutput, cwd);
|
|
625
|
-
if (excerpt) parts.push("", "--- excerpt ---", excerpt);
|
|
626
|
-
return okResult(parts.join("\n"), {
|
|
627
|
-
output: workspaceOutput,
|
|
628
|
-
artifactDir: scoped.artifactDir,
|
|
629
|
-
url,
|
|
630
|
-
mode,
|
|
631
|
-
highlights,
|
|
632
|
-
cacheHit: true,
|
|
633
|
-
cacheKey: cached.cacheKey,
|
|
634
|
-
cachePath: cached.artifactPath,
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
let argv: string[];
|
|
640
|
-
if (mode === "map") {
|
|
641
|
-
argv = [
|
|
642
|
-
"map",
|
|
643
|
-
url,
|
|
644
|
-
"-o",
|
|
645
|
-
output,
|
|
646
|
-
"--limit",
|
|
647
|
-
String(limit),
|
|
648
|
-
...(fast ? ["--fast"] : []),
|
|
649
|
-
];
|
|
650
|
-
} else {
|
|
651
|
-
argv = ["scrape", url, "-o", output, ...(fast ? ["--fast"] : [])];
|
|
652
|
-
if (highlights) {
|
|
653
|
-
if (!hlQuery) {
|
|
654
|
-
return failResult("web_fetch: highlightQuery required when highlights=true");
|
|
655
|
-
}
|
|
656
|
-
argv.push("--highlights", "--highlight-query", hlQuery);
|
|
657
|
-
if (hlScoped) argv.push("--highlights-output", hlScoped.output);
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
const run = runHarnessWeb(MODULE_URL, argv, cwd);
|
|
662
|
-
if (!run.ok) {
|
|
663
|
-
return failResult(
|
|
664
|
-
`web_fetch failed (exit ${run.exitCode}).\n${run.stderr || run.stdout}\n` +
|
|
665
|
-
"Try fast:true for static pages, or run harness-cli-verify for Scrapling install.",
|
|
666
|
-
);
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
const cacheArtifact = highlights && hlScoped ? hlScoped.output : output;
|
|
670
|
-
const cacheWrite = writeFetchCacheEntry(cwd, fetchCtx, cacheArtifact, {
|
|
671
|
-
highlightsPath:
|
|
672
|
-
highlights && hlScoped && hlScoped.output !== cacheArtifact
|
|
673
|
-
? hlScoped.output
|
|
674
|
-
: undefined,
|
|
675
|
-
});
|
|
676
|
-
const workspaceBasename = highlights
|
|
677
|
-
? "highlights.json"
|
|
678
|
-
: mode === "map"
|
|
679
|
-
? "map.json"
|
|
680
|
-
: "page.md";
|
|
681
|
-
publishWorkspaceAlias(cwd, `${cacheWrite.entryDir}/${workspaceBasename}`, workspaceBasename);
|
|
682
|
-
|
|
683
|
-
const parts = [
|
|
684
|
-
run.stdout,
|
|
685
|
-
"",
|
|
686
|
-
`output: ${output}`,
|
|
687
|
-
`artifactDir: ${scoped.artifactDir}`,
|
|
688
|
-
`cache: ${cacheWrite.entryDir}`,
|
|
689
|
-
];
|
|
690
|
-
const excerpt = readTextExcerpt(output, cwd);
|
|
691
|
-
if (excerpt) parts.push("", "--- excerpt ---", excerpt);
|
|
692
|
-
|
|
693
|
-
return okResult(parts.join("\n"), {
|
|
694
|
-
output,
|
|
695
|
-
artifactDir: scoped.artifactDir,
|
|
696
|
-
url,
|
|
697
|
-
mode,
|
|
698
|
-
highlights,
|
|
699
|
-
cacheHit: false,
|
|
700
|
-
cacheKey: cacheWrite.cacheKey,
|
|
701
|
-
cachePath: `${cacheWrite.entryDir}/${workspaceBasename}`,
|
|
702
|
-
});
|
|
945
|
+
return executeWebFetch(params as Record<string, unknown>, ctx);
|
|
703
946
|
},
|
|
704
947
|
});
|
|
705
948
|
|
|
@@ -713,47 +956,7 @@ export default function harnessWebTools(pi: ExtensionAPI) {
|
|
|
713
956
|
parameters: WebFindSimilarSchema,
|
|
714
957
|
|
|
715
958
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
716
|
-
|
|
717
|
-
const webScope = String(params.webScope ?? "").trim() || undefined;
|
|
718
|
-
const url = String(params.url ?? "").trim();
|
|
719
|
-
if (!url) return failResult("web_find_similar: url is required.");
|
|
720
|
-
|
|
721
|
-
const scoped = resolveScopedOutput(
|
|
722
|
-
ctx,
|
|
723
|
-
"search-deep.json",
|
|
724
|
-
params.output ? String(params.output) : undefined,
|
|
725
|
-
webScope,
|
|
726
|
-
);
|
|
727
|
-
const output = scoped.output;
|
|
728
|
-
ensureParentDir(cwd, output);
|
|
729
|
-
const limit = typeof params.limit === "number" ? params.limit : 10;
|
|
730
|
-
const argv = [
|
|
731
|
-
"find-similar",
|
|
732
|
-
url,
|
|
733
|
-
"-o",
|
|
734
|
-
output,
|
|
735
|
-
"--limit",
|
|
736
|
-
String(limit),
|
|
737
|
-
...(params.fast !== false ? ["--fast"] : []),
|
|
738
|
-
];
|
|
739
|
-
|
|
740
|
-
const run = runHarnessWeb(MODULE_URL, argv, cwd);
|
|
741
|
-
if (!run.ok) {
|
|
742
|
-
return failResult(
|
|
743
|
-
`web_find_similar failed (exit ${run.exitCode}).\n${run.stderr || run.stdout}`,
|
|
744
|
-
);
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
const parts = [run.stdout];
|
|
748
|
-
const summary = summarizeDeepSearchJson(output, cwd);
|
|
749
|
-
if (summary) parts.push("", summary);
|
|
750
|
-
parts.push("", `output: ${output}`, `artifactDir: ${scoped.artifactDir}`);
|
|
751
|
-
|
|
752
|
-
return okResult(parts.join("\n"), {
|
|
753
|
-
output,
|
|
754
|
-
artifactDir: scoped.artifactDir,
|
|
755
|
-
url,
|
|
756
|
-
});
|
|
959
|
+
return executeWebFindSimilar(params as Record<string, unknown>, ctx);
|
|
757
960
|
},
|
|
758
961
|
});
|
|
759
962
|
|
|
@@ -767,83 +970,7 @@ export default function harnessWebTools(pi: ExtensionAPI) {
|
|
|
767
970
|
parameters: WebContentsSchema,
|
|
768
971
|
|
|
769
972
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
770
|
-
|
|
771
|
-
const webScope = String(params.webScope ?? "").trim() || undefined;
|
|
772
|
-
const dirScoped = resolveScopedOutput(
|
|
773
|
-
ctx,
|
|
774
|
-
"contents",
|
|
775
|
-
params.outputDir ? String(params.outputDir) : undefined,
|
|
776
|
-
webScope,
|
|
777
|
-
);
|
|
778
|
-
const outputDir = dirScoped.output.endsWith("/contents")
|
|
779
|
-
? dirScoped.output
|
|
780
|
-
: `${dirScoped.artifactDir}/contents`;
|
|
781
|
-
mkdirSync(resolve(cwd, outputDir), { recursive: true });
|
|
782
|
-
let fromSearch = String(params.fromSearch ?? "").trim();
|
|
783
|
-
if (fromSearch && !fromSearch.startsWith("/") && !fromSearch.includes("..")) {
|
|
784
|
-
fromSearch = resolveScopedOutput(
|
|
785
|
-
ctx,
|
|
786
|
-
"search-deep.json",
|
|
787
|
-
fromSearch,
|
|
788
|
-
webScope,
|
|
789
|
-
).output;
|
|
790
|
-
}
|
|
791
|
-
const urls = (params.urls ?? []).map((u) => String(u).trim()).filter(Boolean);
|
|
792
|
-
const limit = typeof params.limit === "number" ? params.limit : 5;
|
|
793
|
-
const hlQuery = String(params.highlightQuery ?? "").trim();
|
|
794
|
-
|
|
795
|
-
const argv = [
|
|
796
|
-
"contents-batch",
|
|
797
|
-
"-o",
|
|
798
|
-
outputDir,
|
|
799
|
-
"--limit",
|
|
800
|
-
String(limit),
|
|
801
|
-
...(params.fast ? ["--fast"] : []),
|
|
802
|
-
...(params.highlights && hlQuery
|
|
803
|
-
? ["--highlights", "--highlight-query", hlQuery]
|
|
804
|
-
: []),
|
|
805
|
-
...urls,
|
|
806
|
-
];
|
|
807
|
-
if (fromSearch) {
|
|
808
|
-
argv.splice(1, 0, "--from-search", fromSearch);
|
|
809
|
-
}
|
|
810
|
-
let evidencePath: string | undefined;
|
|
811
|
-
if (params.evidenceBundle && fromSearch) {
|
|
812
|
-
const bundleArg = String(params.evidenceBundle);
|
|
813
|
-
evidencePath =
|
|
814
|
-
bundleArg.startsWith("/") || bundleArg.includes("..")
|
|
815
|
-
? bundleArg
|
|
816
|
-
: resolveScopedOutput(
|
|
817
|
-
ctx,
|
|
818
|
-
"evidence-bundle.json",
|
|
819
|
-
bundleArg,
|
|
820
|
-
webScope,
|
|
821
|
-
).output;
|
|
822
|
-
ensureParentDir(cwd, evidencePath);
|
|
823
|
-
argv.push("--evidence-bundle", evidencePath);
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
if (!fromSearch && !urls.length) {
|
|
827
|
-
return failResult("web_contents: provide urls or fromSearch");
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
const run = runHarnessWeb(MODULE_URL, argv, cwd);
|
|
831
|
-
if (!run.ok) {
|
|
832
|
-
return failResult(
|
|
833
|
-
`web_contents failed (exit ${run.exitCode}).\n${run.stderr || run.stdout}`,
|
|
834
|
-
);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
return okResult(
|
|
838
|
-
`${run.stdout}\n\noutputDir: ${outputDir}\nartifactDir: ${dirScoped.artifactDir}` +
|
|
839
|
-
(evidencePath ? `\nevidence: ${evidencePath}` : ""),
|
|
840
|
-
{
|
|
841
|
-
outputDir,
|
|
842
|
-
artifactDir: dirScoped.artifactDir,
|
|
843
|
-
fromSearch,
|
|
844
|
-
evidenceBundle: evidencePath,
|
|
845
|
-
},
|
|
846
|
-
);
|
|
973
|
+
return executeWebContents(params as Record<string, unknown>, ctx);
|
|
847
974
|
},
|
|
848
975
|
});
|
|
849
976
|
}
|