ultimate-pi 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/.agents/skills/harness-decisions/SKILL.md +37 -0
  2. package/.agents/skills/harness-governor/SKILL.md +1 -1
  3. package/.agents/skills/harness-orchestration/SKILL.md +54 -0
  4. package/.agents/skills/harness-plan/SKILL.md +4 -3
  5. package/.agents/skills/harness-sentrux-setup/SKILL.md +57 -0
  6. package/.agents/skills/scrapling-web/SKILL.md +93 -0
  7. package/.pi/PACKAGING.md +1 -0
  8. package/.pi/SYSTEM.md +13 -15
  9. package/.pi/agents/harness/adversary.md +3 -0
  10. package/.pi/agents/harness/evaluator.md +3 -0
  11. package/.pi/agents/harness/executor.md +4 -1
  12. package/.pi/agents/harness/meta-optimizer.md +2 -1
  13. package/.pi/agents/harness/planner.md +22 -1
  14. package/.pi/agents/harness/sentrux-bootstrap.md +42 -0
  15. package/.pi/agents/harness/tie-breaker.md +2 -0
  16. package/.pi/extensions/harness-ask-user.ts +74 -0
  17. package/.pi/extensions/harness-subagents.ts +9 -0
  18. package/.pi/extensions/lib/ask-user/dialog.ts +260 -0
  19. package/.pi/extensions/lib/ask-user/fallback.ts +78 -0
  20. package/.pi/extensions/lib/ask-user/render.ts +66 -0
  21. package/.pi/extensions/lib/ask-user/schema.ts +69 -0
  22. package/.pi/extensions/lib/ask-user/types.ts +41 -0
  23. package/.pi/extensions/lib/ask-user/validate-core.mjs +79 -0
  24. package/.pi/extensions/lib/ask-user/validate.ts +92 -0
  25. package/.pi/extensions/lib/harness-subagents/agent-loader.ts +126 -0
  26. package/.pi/extensions/lib/harness-subagents/agent-manifest.ts +119 -0
  27. package/.pi/extensions/lib/harness-subagents/agent-parser.ts +87 -0
  28. package/.pi/extensions/lib/harness-subagents/blackboard-tool.ts +118 -0
  29. package/.pi/extensions/lib/harness-subagents/blackboard.ts +175 -0
  30. package/.pi/extensions/lib/harness-subagents/spawn-policy.ts +27 -0
  31. package/.pi/extensions/lib/harness-subagents/types-blackboard.ts +27 -0
  32. package/.pi/extensions/lib/harness-subagents/vendored/agent-manager.ts +553 -0
  33. package/.pi/extensions/lib/harness-subagents/vendored/agent-runner.ts +637 -0
  34. package/.pi/extensions/lib/harness-subagents/vendored/agent-types.ts +175 -0
  35. package/.pi/extensions/lib/harness-subagents/vendored/context.ts +59 -0
  36. package/.pi/extensions/lib/harness-subagents/vendored/cross-extension-rpc.ts +134 -0
  37. package/.pi/extensions/lib/harness-subagents/vendored/custom-agents.ts +5 -0
  38. package/.pi/extensions/lib/harness-subagents/vendored/default-agents.ts +123 -0
  39. package/.pi/extensions/lib/harness-subagents/vendored/env.ts +43 -0
  40. package/.pi/extensions/lib/harness-subagents/vendored/group-join.ts +144 -0
  41. package/.pi/extensions/lib/harness-subagents/vendored/index.ts +2447 -0
  42. package/.pi/extensions/lib/harness-subagents/vendored/invocation-config.ts +52 -0
  43. package/.pi/extensions/lib/harness-subagents/vendored/memory.ts +182 -0
  44. package/.pi/extensions/lib/harness-subagents/vendored/model-resolver.ts +92 -0
  45. package/.pi/extensions/lib/harness-subagents/vendored/output-file.ts +115 -0
  46. package/.pi/extensions/lib/harness-subagents/vendored/prompts.ts +103 -0
  47. package/.pi/extensions/lib/harness-subagents/vendored/schedule-store.ts +177 -0
  48. package/.pi/extensions/lib/harness-subagents/vendored/schedule.ts +416 -0
  49. package/.pi/extensions/lib/harness-subagents/vendored/settings.ts +210 -0
  50. package/.pi/extensions/lib/harness-subagents/vendored/skill-loader.ts +108 -0
  51. package/.pi/extensions/lib/harness-subagents/vendored/types.ts +187 -0
  52. package/.pi/extensions/lib/harness-subagents/vendored/ui/agent-widget.ts +637 -0
  53. package/.pi/extensions/lib/harness-subagents/vendored/ui/conversation-viewer.ts +324 -0
  54. package/.pi/extensions/lib/harness-subagents/vendored/ui/schedule-menu.ts +110 -0
  55. package/.pi/extensions/lib/harness-subagents/vendored/usage.ts +71 -0
  56. package/.pi/extensions/lib/harness-subagents/vendored/worktree.ts +195 -0
  57. package/.pi/extensions/lib/harness-vcc-settings.ts +50 -0
  58. package/.pi/extensions/ultimate-pi-vcc.ts +17 -0
  59. package/.pi/harness/README.md +2 -1
  60. package/.pi/harness/agents.manifest.json +80 -0
  61. package/.pi/harness/docs/adrs/0009-sentrux-rules-lifecycle.md +9 -5
  62. package/.pi/harness/docs/adrs/0030-inhouse-vcc-compaction.md +40 -0
  63. package/.pi/harness/docs/adrs/README.md +1 -0
  64. package/.pi/harness/env.harness.template +28 -0
  65. package/.pi/harness/sentrux/architecture.manifest.json +6 -1
  66. package/.pi/prompts/harness-auto.md +2 -2
  67. package/.pi/prompts/harness-plan.md +2 -2
  68. package/.pi/prompts/harness-router-tune.md +2 -2
  69. package/.pi/prompts/harness-run.md +1 -0
  70. package/.pi/prompts/harness-setup.md +179 -340
  71. package/.pi/scripts/README.md +6 -1
  72. package/.pi/scripts/harness-agents-manifest.mjs +123 -0
  73. package/.pi/scripts/harness-cli-verify.sh +60 -11
  74. package/.pi/scripts/harness-generate-model-router.mjs +242 -0
  75. package/.pi/scripts/harness-graphify-bootstrap.sh +1 -6
  76. package/.pi/scripts/harness-resolve-up-pkg.mjs +71 -0
  77. package/.pi/scripts/harness-seed-project-contracts.mjs +33 -1
  78. package/.pi/scripts/harness-sentrux-bootstrap.mjs +146 -0
  79. package/.pi/scripts/harness-sync-env.mjs +148 -0
  80. package/.pi/scripts/harness-verify.mjs +19 -0
  81. package/.pi/scripts/harness-web-search.md +33 -0
  82. package/.pi/scripts/harness-web.py +177 -0
  83. package/.pi/scripts/harness_web/__init__.py +1 -0
  84. package/.pi/scripts/harness_web/config.py +80 -0
  85. package/.pi/scripts/harness_web/output.py +55 -0
  86. package/.pi/scripts/harness_web/scrape.py +120 -0
  87. package/.pi/scripts/harness_web/search_ddg.py +106 -0
  88. package/.pi/scripts/release.sh +338 -0
  89. package/.pi/scripts/sentrux-rules-sync.mjs +29 -7
  90. package/.pi/scripts/vendor-pi-vcc-settings.stub.ts +8 -0
  91. package/.pi/scripts/vendor-sync-pi-vcc.sh +40 -0
  92. package/.pi/settings.example.json +1 -7
  93. package/.sentrux/rules.toml +1 -1
  94. package/AGENTS.md +1 -1
  95. package/CHANGELOG.md +14 -0
  96. package/THIRD_PARTY_NOTICES.md +8 -0
  97. package/package.json +16 -12
  98. package/vendor/pi-vcc/README.md +215 -0
  99. package/vendor/pi-vcc/UPSTREAM_PIN.md +12 -0
  100. package/vendor/pi-vcc/demo.gif +0 -0
  101. package/vendor/pi-vcc/index.ts +12 -0
  102. package/vendor/pi-vcc/package.json +26 -0
  103. package/vendor/pi-vcc/scripts/audit-sessions.ts +88 -0
  104. package/vendor/pi-vcc/scripts/benchmark-real-sessions.ts +25 -0
  105. package/vendor/pi-vcc/scripts/compare-before-after.ts +36 -0
  106. package/vendor/pi-vcc/scripts/dump-branch-output.ts +20 -0
  107. package/vendor/pi-vcc/src/commands/pi-vcc.ts +36 -0
  108. package/vendor/pi-vcc/src/commands/vcc-recall.ts +65 -0
  109. package/vendor/pi-vcc/src/core/brief.ts +381 -0
  110. package/vendor/pi-vcc/src/core/build-sections.ts +79 -0
  111. package/vendor/pi-vcc/src/core/content.ts +60 -0
  112. package/vendor/pi-vcc/src/core/filter-noise.ts +42 -0
  113. package/vendor/pi-vcc/src/core/format-recall.ts +27 -0
  114. package/vendor/pi-vcc/src/core/format.ts +49 -0
  115. package/vendor/pi-vcc/src/core/lineage.ts +26 -0
  116. package/vendor/pi-vcc/src/core/load-messages.ts +41 -0
  117. package/vendor/pi-vcc/src/core/normalize.ts +66 -0
  118. package/vendor/pi-vcc/src/core/recall-scope.ts +14 -0
  119. package/vendor/pi-vcc/src/core/render-entries.ts +55 -0
  120. package/vendor/pi-vcc/src/core/report.ts +237 -0
  121. package/vendor/pi-vcc/src/core/sanitize.ts +5 -0
  122. package/vendor/pi-vcc/src/core/search-entries.ts +221 -0
  123. package/vendor/pi-vcc/src/core/settings.ts +8 -0
  124. package/vendor/pi-vcc/src/core/skill-collapse.ts +35 -0
  125. package/vendor/pi-vcc/src/core/summarize.ts +157 -0
  126. package/vendor/pi-vcc/src/core/tool-args.ts +14 -0
  127. package/vendor/pi-vcc/src/details.ts +7 -0
  128. package/vendor/pi-vcc/src/extract/commits.ts +69 -0
  129. package/vendor/pi-vcc/src/extract/files.ts +80 -0
  130. package/vendor/pi-vcc/src/extract/goals.ts +79 -0
  131. package/vendor/pi-vcc/src/extract/preferences.ts +55 -0
  132. package/vendor/pi-vcc/src/hooks/before-compact.ts +314 -0
  133. package/vendor/pi-vcc/src/sections.ts +12 -0
  134. package/vendor/pi-vcc/src/tools/recall.ts +109 -0
  135. package/vendor/pi-vcc/src/types.ts +14 -0
  136. package/vendor/pi-vcc/tests/before-compact-hook.test.ts +204 -0
  137. package/vendor/pi-vcc/tests/before-compact.test.ts +145 -0
  138. package/vendor/pi-vcc/tests/brief.test.ts +206 -0
  139. package/vendor/pi-vcc/tests/build-sections.test.ts +59 -0
  140. package/vendor/pi-vcc/tests/compile.test.ts +80 -0
  141. package/vendor/pi-vcc/tests/content.test.ts +31 -0
  142. package/vendor/pi-vcc/tests/extract-goals.test.ts +86 -0
  143. package/vendor/pi-vcc/tests/extract-preferences.test.ts +30 -0
  144. package/vendor/pi-vcc/tests/filter-noise.test.ts +61 -0
  145. package/vendor/pi-vcc/tests/fixtures.ts +61 -0
  146. package/vendor/pi-vcc/tests/format-recall.test.ts +30 -0
  147. package/vendor/pi-vcc/tests/format.test.ts +62 -0
  148. package/vendor/pi-vcc/tests/lineage.test.ts +33 -0
  149. package/vendor/pi-vcc/tests/load-messages.test.ts +51 -0
  150. package/vendor/pi-vcc/tests/normalize.test.ts +97 -0
  151. package/vendor/pi-vcc/tests/real-sessions.test.ts +38 -0
  152. package/vendor/pi-vcc/tests/recall-expand.test.ts +15 -0
  153. package/vendor/pi-vcc/tests/recall-scope.test.ts +32 -0
  154. package/vendor/pi-vcc/tests/recall-tool-scope.test.ts +67 -0
  155. package/vendor/pi-vcc/tests/render-entries.test.ts +62 -0
  156. package/vendor/pi-vcc/tests/report.test.ts +44 -0
  157. package/vendor/pi-vcc/tests/sanitize.test.ts +24 -0
  158. package/vendor/pi-vcc/tests/search-entries.test.ts +144 -0
  159. package/vendor/pi-vcc/tests/support/load-session.ts +23 -0
  160. package/vendor/pi-vcc/tests/support/real-sessions.ts +51 -0
  161. package/.agents/skills/firecrawl/SKILL.md +0 -150
  162. package/.agents/skills/firecrawl/rules/install.md +0 -82
  163. package/.agents/skills/firecrawl/rules/security.md +0 -26
  164. package/.agents/skills/firecrawl-agent/SKILL.md +0 -57
  165. package/.agents/skills/firecrawl-build-interact/SKILL.md +0 -67
  166. package/.agents/skills/firecrawl-build-onboarding/SKILL.md +0 -102
  167. package/.agents/skills/firecrawl-build-onboarding/references/auth-flow.md +0 -39
  168. package/.agents/skills/firecrawl-build-onboarding/references/project-setup.md +0 -20
  169. package/.agents/skills/firecrawl-build-onboarding/references/sdk-installation.md +0 -17
  170. package/.agents/skills/firecrawl-build-scrape/SKILL.md +0 -68
  171. package/.agents/skills/firecrawl-build-search/SKILL.md +0 -68
  172. package/.agents/skills/firecrawl-crawl/SKILL.md +0 -58
  173. package/.agents/skills/firecrawl-download/SKILL.md +0 -69
  174. package/.agents/skills/firecrawl-interact/SKILL.md +0 -83
  175. package/.agents/skills/firecrawl-map/SKILL.md +0 -50
  176. package/.agents/skills/firecrawl-parse/SKILL.md +0 -61
  177. package/.agents/skills/firecrawl-scrape/SKILL.md +0 -68
  178. package/.agents/skills/firecrawl-search/SKILL.md +0 -59
  179. package/.pi/pi-vcc-config.json +0 -4
  180. package/firecrawl/.env.template +0 -62
  181. package/firecrawl/README.md +0 -49
  182. package/firecrawl/docker-compose.yaml +0 -201
  183. package/firecrawl/searxng/searxng.env +0 -3
  184. package/firecrawl/searxng/settings.yml +0 -85
@@ -0,0 +1,15 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { invalidExpandIndices } from "../src/tools/recall";
3
+
4
+ describe("invalidExpandIndices", () => {
5
+ it("returns indices that are not in available lineage index set", () => {
6
+ const available = new Set([0, 2, 5]);
7
+ expect(invalidExpandIndices([0, 2], available)).toEqual([]);
8
+ expect(invalidExpandIndices([1, 2, 7], available)).toEqual([1, 7]);
9
+ });
10
+
11
+ it("rejects non-integer indices", () => {
12
+ const available = new Set([0, 1, 2]);
13
+ expect(invalidExpandIndices([1.5, 2], available)).toEqual([1.5]);
14
+ });
15
+ });
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { normalizeRecallScope, parseRecallScope } from "../src/core/recall-scope";
3
+
4
+ describe("normalizeRecallScope", () => {
5
+ it("defaults to active lineage", () => {
6
+ expect(normalizeRecallScope()).toBe("lineage");
7
+ expect(normalizeRecallScope("lineage")).toBe("lineage");
8
+ expect(normalizeRecallScope("unknown")).toBe("lineage");
9
+ expect(normalizeRecallScope(123)).toBe("lineage");
10
+ });
11
+
12
+ it("accepts all scope", () => {
13
+ expect(normalizeRecallScope("all")).toBe("all");
14
+ expect(normalizeRecallScope("ALL")).toBe("all");
15
+ });
16
+ });
17
+
18
+ describe("parseRecallScope", () => {
19
+ it("removes scope token from command text", () => {
20
+ expect(parseRecallScope("license scope:all page:2")).toEqual({
21
+ scope: "all",
22
+ text: "license page:2",
23
+ });
24
+ });
25
+
26
+ it("defaults to lineage when no scope token is present", () => {
27
+ expect(parseRecallScope("license page:2")).toEqual({
28
+ scope: "lineage",
29
+ text: "license page:2",
30
+ });
31
+ });
32
+ });
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { mkdtempSync, writeFileSync, rmSync } from "fs";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+ import { registerRecallTool } from "../src/tools/recall";
6
+
7
+ const makeSession = () => {
8
+ const dir = mkdtempSync(join(tmpdir(), "pi-vcc-recall-scope-"));
9
+ const file = join(dir, "session.jsonl");
10
+ const lines = [
11
+ JSON.stringify({ type: "message", id: "m1", message: { role: "user", content: "active lineage token" } }),
12
+ JSON.stringify({ type: "message", id: "m2", message: { role: "user", content: "off lineage secret" } }),
13
+ ];
14
+ writeFileSync(file, lines.join("\n") + "\n", "utf8");
15
+ return { dir, file };
16
+ };
17
+
18
+ const register = () => {
19
+ let tool: any;
20
+ registerRecallTool({ registerTool: (t: any) => { tool = t; } } as any);
21
+ return tool;
22
+ };
23
+
24
+ const invoke = async (tool: any, file: string, params: Record<string, unknown>) => {
25
+ const result = await tool.execute("tool-call", params, undefined, undefined, {
26
+ sessionManager: {
27
+ getSessionFile: () => file,
28
+ getBranch: () => [{ id: "m1" }],
29
+ getEntries: () => [{ id: "m1" }, { id: "m2" }],
30
+ },
31
+ });
32
+ return result.content[0].text as string;
33
+ };
34
+
35
+ describe("vcc_recall scope", () => {
36
+ it("defaults to active lineage and opts into all-session search explicitly", async () => {
37
+ const { dir, file } = makeSession();
38
+ try {
39
+ const tool = register();
40
+
41
+ const lineage = await invoke(tool, file, { query: "secret" });
42
+ expect(lineage).toContain("No matches");
43
+
44
+ const all = await invoke(tool, file, { query: "secret", scope: "all" });
45
+ expect(all).toContain("scope: all");
46
+ expect(all).toContain("off lineage secret");
47
+ } finally {
48
+ rmSync(dir, { recursive: true, force: true });
49
+ }
50
+ });
51
+
52
+ it("keeps expand strict by default but allows off-lineage expand with scope all", async () => {
53
+ const { dir, file } = makeSession();
54
+ try {
55
+ const tool = register();
56
+
57
+ const lineage = await invoke(tool, file, { expand: [1] });
58
+ expect(lineage).toContain("Cannot expand indices outside active lineage: 1");
59
+
60
+ const all = await invoke(tool, file, { expand: [1], scope: "all" });
61
+ expect(all).toContain("Scope: all");
62
+ expect(all).toContain("#1 [user] off lineage secret");
63
+ } finally {
64
+ rmSync(dir, { recursive: true, force: true });
65
+ }
66
+ });
67
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { renderMessage } from "../src/core/render-entries";
3
+ import type { Message } from "@mariozechner/pi-ai";
4
+ import { userMsg, assistantText, assistantWithToolCall, toolResult } from "./fixtures";
5
+
6
+ describe("renderMessage", () => {
7
+ it("renders user message", () => {
8
+ const r = renderMessage(userMsg("hello"), 0);
9
+ expect(r).toEqual({ index: 0, role: "user", summary: "hello" });
10
+ });
11
+
12
+ it("renders assistant text", () => {
13
+ const r = renderMessage(assistantText("done"), 1);
14
+ expect(r.role).toBe("assistant");
15
+ expect(r.summary).toBe("done");
16
+ });
17
+
18
+ it("renders tool result", () => {
19
+ const r = renderMessage(toolResult("Read", "file contents"), 2);
20
+ expect(r.role).toBe("tool_result");
21
+ expect(r.summary).toContain("[Read]");
22
+ });
23
+
24
+ it("renders tool call arguments with values", () => {
25
+ const r = renderMessage(assistantWithToolCall("Read", { path: "a.ts" }), 2);
26
+ expect(r.summary).toContain("Read(path=a.ts)");
27
+ });
28
+
29
+ it("renders error tool result with prefix", () => {
30
+ const r = renderMessage(toolResult("bash", "not found", true), 3);
31
+ expect(r.summary).toStartWith("ERROR");
32
+ });
33
+
34
+ it("truncates long user text", () => {
35
+ const long = "x".repeat(500);
36
+ const r = renderMessage(userMsg(long), 0);
37
+ expect(r.summary.length).toBeLessThanOrEqual(300);
38
+ });
39
+
40
+ it("renders bashExecution message", () => {
41
+ const msg = { role: "bashExecution", command: "ls -la", output: "total 0\n" } as any;
42
+ const r = renderMessage(msg, 5);
43
+ expect(r.role).toBe("bash");
44
+ expect(r.summary).toContain("$ ls -la");
45
+ expect(r.summary).toContain("total 0");
46
+ });
47
+
48
+ it("renders bashExecution with missing output", () => {
49
+ const msg = { role: "bashExecution", command: "exit 1" } as any;
50
+ const r = renderMessage(msg, 6);
51
+ expect(r.role).toBe("bash");
52
+ expect(r.summary).toContain("$ exit 1");
53
+ });
54
+
55
+ it("handles message with undefined content", () => {
56
+ const msg = { role: "assistant", content: undefined } as any;
57
+ const r = renderMessage(msg, 3);
58
+ expect(r.role).toBe("assistant");
59
+ expect(r.summary).toBe("");
60
+ });
61
+ });
62
+
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { buildCompactReport } from "../src/core/report";
3
+ import {
4
+ userMsg,
5
+ assistantText,
6
+ assistantWithToolCall,
7
+ toolResult,
8
+ } from "./fixtures";
9
+
10
+ describe("buildCompactReport", () => {
11
+ it("includes before and after compact metrics", () => {
12
+ const report = buildCompactReport({
13
+ messages: [
14
+ userMsg("Fix login bug in auth.ts"),
15
+ assistantWithToolCall("Read", { path: "auth.ts" }),
16
+ toolResult("Read", "const x = 1;"),
17
+ assistantText("Found the root cause in auth.ts.\n1. Fix validation\n2. Run tests"),
18
+ ],
19
+ });
20
+ expect(report.before.messageCount).toBe(4);
21
+ expect(report.before.roleCounts.user).toBe(1);
22
+ expect(report.before.topFiles).toContain("auth.ts");
23
+ expect(report.before.preview).toContain("Fix login bug in auth.ts");
24
+ expect(report.after.sectionCount).toBeGreaterThan(0);
25
+ expect(report.after.summaryPreview).toContain("[Session Goal]");
26
+ expect(report.after.summaryPreview).toContain('* Read "auth.ts"');
27
+ expect(report.after.briefTranscriptLines).toBeGreaterThan(0);
28
+ expect(report.compression.ratio).toBeGreaterThan(0);
29
+ });
30
+
31
+ it("marks recall probe coverage for goal and file queries", () => {
32
+ const report = buildCompactReport({
33
+ messages: [
34
+ userMsg("Fix login bug in auth.ts"),
35
+ assistantWithToolCall("Read", { path: "auth.ts" }),
36
+ toolResult("Read", "code content"),
37
+ assistantText("Found the root cause"),
38
+ ],
39
+ });
40
+ expect(report.recall.probes.length).toBeGreaterThanOrEqual(1);
41
+ const goalProbe = report.recall.probes.find((p) => p.label === "goal");
42
+ expect(goalProbe?.summaryMentioned).toBe(true);
43
+ });
44
+ });
@@ -0,0 +1,24 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { sanitize } from "../src/core/sanitize";
3
+
4
+ describe("sanitize", () => {
5
+ it("strips ANSI escape codes", () => {
6
+ expect(sanitize("\x1b[31mred\x1b[0m")).toBe("red");
7
+ });
8
+
9
+ it("normalizes CRLF to LF", () => {
10
+ expect(sanitize("a\r\nb\r\n")).toBe("a\nb\n");
11
+ });
12
+
13
+ it("strips bare CR", () => {
14
+ expect(sanitize("a\rb")).toBe("a\nb");
15
+ });
16
+
17
+ it("strips control characters but preserves newlines and tabs", () => {
18
+ expect(sanitize("a\x00b\tc\nd")).toBe("ab\tc\nd");
19
+ });
20
+
21
+ it("passes clean text unchanged", () => {
22
+ expect(sanitize("hello world")).toBe("hello world");
23
+ });
24
+ });
@@ -0,0 +1,144 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { searchEntries } from "../src/core/search-entries";
3
+ import type { RenderedEntry } from "../src/core/render-entries";
4
+ import type { Message } from "@mariozechner/pi-ai";
5
+
6
+ const entries: RenderedEntry[] = [
7
+ { index: 0, role: "user", summary: "Fix login bug" },
8
+ { index: 1, role: "assistant", summary: "Reading auth.ts" },
9
+ { index: 2, role: "tool_result", summary: "[Read] code here" },
10
+ { index: 3, role: "assistant", summary: "Found the root cause in auth module" },
11
+ ];
12
+
13
+ const messages: Message[] = [
14
+ { role: "user", content: "Fix login bug" } as any,
15
+ { role: "assistant", content: [{ type: "text", text: "Reading auth.ts" }] } as any,
16
+ { role: "toolResult", content: [{ type: "text", text: "[Read] code here" }] } as any,
17
+ { role: "assistant", content: [{ type: "text", text: "Found the root cause in auth module" }] } as any,
18
+ ];
19
+
20
+ describe("searchEntries", () => {
21
+ it("returns all for empty query", () => {
22
+ expect(searchEntries(entries, messages)).toEqual(entries);
23
+ expect(searchEntries(entries, messages, "")).toEqual(entries);
24
+ });
25
+
26
+ it("filters by single term", () => {
27
+ const r = searchEntries(entries, messages, "login");
28
+ expect(r).toHaveLength(1);
29
+ expect(r[0].index).toBe(0);
30
+ });
31
+
32
+ it("returns empty for no match", () => {
33
+ expect(searchEntries(entries, messages, "xyz123")).toEqual([]);
34
+ });
35
+
36
+ it("finds keyword beyond clip boundary in full content", () => {
37
+ const longText = "A".repeat(400) + " hidden_keyword here";
38
+ const longEntries: RenderedEntry[] = [
39
+ { index: 0, role: "user", summary: "A".repeat(300) },
40
+ ];
41
+ const longMsgs: Message[] = [
42
+ { role: "user", content: longText } as any,
43
+ ];
44
+ const r = searchEntries(longEntries, longMsgs, "hidden_keyword");
45
+ expect(r).toHaveLength(1);
46
+ expect(r[0].snippet).toContain("hidden_keyword");
47
+ });
48
+
49
+ it("returns snippet around matched term", () => {
50
+ const r = searchEntries(entries, messages, "root");
51
+ expect(r).toHaveLength(1);
52
+ expect(r[0].snippet).toBeDefined();
53
+ expect(r[0].snippet).toContain("root");
54
+ });
55
+
56
+ // ── regex support ──
57
+
58
+ it("supports regex pattern: alternation", () => {
59
+ const r = searchEntries(entries, messages, "login|auth");
60
+ expect(r).toHaveLength(3); // "login bug", "auth.ts", "auth module"
61
+ expect(r.map((h) => h.index).sort()).toEqual([0, 1, 3]);
62
+ });
63
+
64
+ it("supports regex pattern: wildcard", () => {
65
+ const r = searchEntries(entries, messages, "Read.*auth");
66
+ expect(r).toHaveLength(1);
67
+ expect(r[0].index).toBe(1);
68
+ });
69
+
70
+ it("falls back to escaped literal for invalid regex", () => {
71
+ const extraEntries: RenderedEntry[] = [
72
+ { index: 0, role: "user", summary: "test (foo" },
73
+ { index: 1, role: "assistant", summary: "no match here" },
74
+ ];
75
+ const extraMsgs: Message[] = [
76
+ { role: "user", content: "error with (foo pattern" } as any,
77
+ { role: "assistant", content: [{ type: "text", text: "no match here" }] } as any,
78
+ ];
79
+ const r = searchEntries(extraEntries, extraMsgs, "(foo");
80
+ expect(r).toHaveLength(1);
81
+ expect(r[0].index).toBe(0);
82
+ });
83
+
84
+ it("regex is case-insensitive", () => {
85
+ const r = searchEntries(entries, messages, "FIX|ROOT");
86
+ expect(r).toHaveLength(2);
87
+ });
88
+
89
+ // ── natural language queries (OR logic + ranking) ──
90
+
91
+ it("natural language query uses OR logic", () => {
92
+ // "root cause auth" -- matches entries containing ANY of these terms
93
+ const r = searchEntries(entries, messages, "root cause auth");
94
+ expect(r.length).toBeGreaterThanOrEqual(2); // #3 has all 3, #1 has auth
95
+ // Best match (highest BM25) should come first
96
+ expect(r[0].index).toBe(3); // "Found the root cause in auth module" matches all 3
97
+ });
98
+
99
+ it("natural language ranks by BM25 score", () => {
100
+ const r = searchEntries(entries, messages, "root cause auth");
101
+ // Top result has more terms matched = higher BM25 score
102
+ expect(r[0].matchCount!).toBeGreaterThanOrEqual(r[r.length - 1].matchCount!);
103
+ });
104
+
105
+ it("filters stopwords from queries", () => {
106
+ // "the root cause of it" → stopwords: the, of, it → meaningful: root, cause
107
+ const r = searchEntries(entries, messages, "the root cause of it");
108
+ expect(r).toHaveLength(1);
109
+ expect(r[0].index).toBe(3);
110
+ });
111
+
112
+ it("keeps all terms if all are stopwords", () => {
113
+ // When all terms are stopwords, keep them (don't drop everything)
114
+ // "the" appears in "Found the root cause" so it matches
115
+ const r = searchEntries(entries, messages, "the");
116
+ expect(r.length).toBeGreaterThan(0);
117
+ });
118
+
119
+ // ── line-based snippet ──
120
+
121
+ it("snippet shows context lines around match", () => {
122
+ const multiline = "line 0\nline 1\nline 2 TARGET\nline 3\nline 4\nline 5";
123
+ const e: RenderedEntry[] = [{ index: 0, role: "user", summary: "test" }];
124
+ const m: Message[] = [{ role: "user", content: multiline } as any];
125
+ const r = searchEntries(e, m, "TARGET");
126
+ expect(r).toHaveLength(1);
127
+ const snip = r[0].snippet!;
128
+ expect(snip).toContain("line 2 TARGET");
129
+ expect(snip).toContain("line 0");
130
+ expect(snip).toContain("line 4");
131
+ expect(snip).not.toContain("line 5");
132
+ });
133
+
134
+ it("snippet handles match at beginning", () => {
135
+ const multiline = "TARGET here\nline 1\nline 2\nline 3";
136
+ const e: RenderedEntry[] = [{ index: 0, role: "user", summary: "test" }];
137
+ const m: Message[] = [{ role: "user", content: multiline } as any];
138
+ const r = searchEntries(e, m, "TARGET");
139
+ const snip = r[0].snippet!;
140
+ expect(snip).toContain("TARGET here");
141
+ expect(snip).toContain("line 2");
142
+ expect(snip).not.toContain("line 3");
143
+ });
144
+ });
@@ -0,0 +1,23 @@
1
+ import { buildSessionContext, loadEntriesFromFile } from "../../node_modules/@mariozechner/pi-coding-agent/dist/core/session-manager.js";
2
+ import type { Message } from "@mariozechner/pi-ai";
3
+
4
+ export interface LoadedSession {
5
+ messageCount: number;
6
+ skippedCount: number;
7
+ messages: Message[];
8
+ }
9
+
10
+ export const loadSessionMessages = (file: string): LoadedSession => {
11
+ const entries = loadEntriesFromFile(file);
12
+ const sessionEntries = entries.filter((entry) => entry.type !== "header");
13
+ const context = buildSessionContext(sessionEntries as any);
14
+ const messages = (context.messages as any[]).filter(
15
+ (msg): msg is Message =>
16
+ msg && typeof msg.role === "string" && "content" in msg,
17
+ );
18
+ return {
19
+ messageCount: messages.length,
20
+ skippedCount: context.messages.length - messages.length,
21
+ messages,
22
+ };
23
+ };
@@ -0,0 +1,51 @@
1
+ import { mkdir, mkdtemp, copyFile, chmod, readdir, stat } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join, basename } from "node:path";
4
+
5
+ const SESSION_ROOT = join(process.env.HOME ?? "", ".pi/agent/sessions");
6
+
7
+ export interface SessionSample {
8
+ source: string;
9
+ copy: string;
10
+ size: number;
11
+ mtimeMs: number;
12
+ }
13
+
14
+ const walk = async (dir: string): Promise<string[]> => {
15
+ const names = await readdir(dir, { withFileTypes: true });
16
+ const out: string[] = [];
17
+ for (const name of names) {
18
+ const path = join(dir, name.name);
19
+ if (name.isDirectory()) out.push(...await walk(path));
20
+ else if (name.isFile() && path.endsWith(".jsonl")) out.push(path);
21
+ }
22
+ return out;
23
+ };
24
+
25
+ const pickLargest = async (limit: number): Promise<string[]> => {
26
+ const files = await walk(SESSION_ROOT);
27
+ const sized = await Promise.all(
28
+ files.map(async (file) => ({ file, size: (await stat(file)).size })),
29
+ );
30
+ return sized.sort((a, b) => b.size - a.size).slice(0, limit).map((x) => x.file);
31
+ };
32
+
33
+ export const prepareSessionSamples = async (limit = 2): Promise<SessionSample[]> => {
34
+ const selected = await pickLargest(limit);
35
+ const dir = await mkdtemp(join(tmpdir(), "pi-vcc-sessions-"));
36
+ await mkdir(dir, { recursive: true });
37
+ const samples: SessionSample[] = [];
38
+ for (const source of selected) {
39
+ const srcStat = await stat(source);
40
+ const copy = join(dir, basename(source));
41
+ await copyFile(source, copy);
42
+ await chmod(copy, 0o444);
43
+ samples.push({ source, copy, size: srcStat.size, mtimeMs: srcStat.mtimeMs });
44
+ }
45
+ return samples;
46
+ };
47
+
48
+ export const readSourceStat = async (sample: SessionSample) => {
49
+ const s = await stat(sample.source);
50
+ return { size: s.size, mtimeMs: s.mtimeMs };
51
+ };
@@ -1,150 +0,0 @@
1
- ---
2
- name: firecrawl
3
- description: |
4
- Search, scrape, and interact with the web via the Firecrawl CLI. Use this skill whenever the user wants to search the web, find articles, research a topic, look something up online, scrape a webpage, grab content from a URL, get data from a website, crawl documentation, download a site, or interact with pages that need clicks or logins. Also use when they say "fetch this page", "pull the content from", "get the page at https://", or reference external websites. This provides real-time web search with full page content and interact capabilities — beyond what Claude can do natively with built-in tools. Do NOT trigger for local file operations, git commands, deployments, or code editing tasks.
5
- allowed-tools:
6
- - Bash(firecrawl *)
7
- - Bash(npx firecrawl *)
8
- ---
9
-
10
- # Firecrawl CLI
11
-
12
- Search, scrape, and interact with the web. Returns clean markdown optimized for LLM context windows.
13
-
14
- Run `firecrawl --help` or `firecrawl <command> --help` for full option details.
15
-
16
- If the task is to integrate Firecrawl into an application, add `FIRECRAWL_API_KEY` to a project, or choose endpoint usage in product code, use the `firecrawl-build` skills. They are already installed alongside this CLI skill when you run `firecrawl init`.
17
-
18
- ## Prerequisites
19
-
20
- Must be installed and authenticated. Check with `firecrawl --status`.
21
-
22
- ```
23
- 🔥 firecrawl cli v1.8.0
24
-
25
- ● Authenticated via FIRECRAWL_API_KEY
26
- Concurrency: 0/100 jobs (parallel scrape limit)
27
- Credits: 500,000 remaining
28
- ```
29
-
30
- - **Concurrency**: Max parallel jobs. Run parallel operations up to this limit.
31
- - **Credits**: Remaining API credits. Each operation consumes credits.
32
-
33
- If not ready, see [rules/install.md](rules/install.md). For output handling guidelines, see [rules/security.md](rules/security.md).
34
-
35
- Before doing real work, verify the setup with one small request:
36
-
37
- ```bash
38
- mkdir -p .firecrawl
39
- firecrawl scrape "https://firecrawl.dev" -o .firecrawl/install-check.md
40
- ```
41
-
42
- ```bash
43
- firecrawl search "query" --scrape --limit 3
44
- ```
45
-
46
- ## Workflow
47
-
48
- Follow this escalation pattern:
49
-
50
- 1. **Search** - No specific URL yet. Find pages, answer questions, discover sources.
51
- 2. **Scrape** - Have a URL. Extract its content directly.
52
- 3. **Map + Scrape** - Large site or need a specific subpage. Use `map --search` to find the right URL, then scrape it.
53
- 4. **Crawl** - Need bulk content from an entire site section (e.g., all /docs/).
54
- 5. **Interact** - Scrape first, then interact with the page (pagination, modals, form submissions, multi-step navigation).
55
-
56
- | Need | Command | When |
57
- | --------------------------- | --------------------- | --------------------------------------------------------- |
58
- | Find pages on a topic | `search` | No specific URL yet |
59
- | Get a page's content | `scrape` | Have a URL, page is static or JS-rendered |
60
- | Find URLs within a site | `map` | Need to locate a specific subpage |
61
- | Bulk extract a site section | `crawl` | Need many pages (e.g., all /docs/) |
62
- | AI-powered data extraction | `agent` | Need structured data from complex sites |
63
- | Interact with a page | `scrape` + `interact` | Content requires clicks, form fills, pagination, or login |
64
- | Download a site to files | `download` | Save an entire site as local files |
65
- | Parse a local file | `parse` | File on disk (PDF, DOCX, XLSX, etc.) — not a URL |
66
-
67
- For detailed command reference, run `firecrawl <command> --help`.
68
-
69
- **Scrape vs interact:**
70
-
71
- - Use `scrape` first. It handles static pages and JS-rendered SPAs.
72
- - Use `scrape` + `interact` when you need to interact with a page, such as clicking buttons, filling out forms, navigating through a complex site, infinite scroll, or when scrape fails to grab all the content you need.
73
- - Never use interact for web searches - use `search` instead.
74
-
75
- **Avoid redundant fetches:**
76
-
77
- - `search --scrape` already fetches full page content. Don't re-scrape those URLs.
78
- - Check `.firecrawl/` for existing data before fetching again.
79
-
80
- ## When to Load References
81
-
82
- - **Searching the web or finding sources first** -> [firecrawl-search](../firecrawl-search/SKILL.md)
83
- - **Scraping a known URL** -> [firecrawl-scrape](../firecrawl-scrape/SKILL.md)
84
- - **Finding URLs on a known site** -> [firecrawl-map](../firecrawl-map/SKILL.md)
85
- - **Bulk extraction from a docs section or site** -> [firecrawl-crawl](../firecrawl-crawl/SKILL.md)
86
- - **AI-powered structured extraction from complex sites** -> [firecrawl-agent](../firecrawl-agent/SKILL.md)
87
- - **Clicks, forms, login, pagination, or post-scrape browser actions** -> [firecrawl-interact](../firecrawl-interact/SKILL.md)
88
- - **Downloading a site to local files** -> [firecrawl-download](../firecrawl-download/SKILL.md)
89
- - **Parsing a local file (PDF, DOCX, XLSX, HTML, etc.)** -> [firecrawl-parse](../firecrawl-parse/SKILL.md)
90
- - **Install, auth, or setup problems** -> [rules/install.md](rules/install.md)
91
- - **Output handling and safe file-reading patterns** -> [rules/security.md](rules/security.md)
92
- - **Integrating Firecrawl into an app, adding `FIRECRAWL_API_KEY` to `.env`, or choosing endpoint usage in product code** -> use the `firecrawl-build` skills (already installed alongside this CLI skill)
93
-
94
- ## Output & Organization
95
-
96
- Unless the user specifies to return in context, write results to `.firecrawl/` with `-o`. Add `.firecrawl/` to `.gitignore`. Always quote URLs - shell interprets `?` and `&` as special characters.
97
-
98
- ```bash
99
- firecrawl search "react hooks" -o .firecrawl/search-react-hooks.json --json
100
- firecrawl scrape "<url>" -o .firecrawl/page.md
101
- ```
102
-
103
- Naming conventions:
104
-
105
- ```
106
- .firecrawl/search-{query}.json
107
- .firecrawl/search-{query}-scraped.json
108
- .firecrawl/{site}-{path}.md
109
- ```
110
-
111
- Never read entire output files at once. Use `grep`, `head`, or incremental reads:
112
-
113
- ```bash
114
- wc -l .firecrawl/file.md && head -50 .firecrawl/file.md
115
- grep -n "keyword" .firecrawl/file.md
116
- ```
117
-
118
- Single format outputs raw content. Multiple formats (e.g., `--format markdown,links`) output JSON.
119
-
120
- ## Working with Results
121
-
122
- These patterns are useful when working with file-based output (`-o` flag) for complex tasks:
123
-
124
- ```bash
125
- # Extract URLs from search
126
- jq -r '.data.web[].url' .firecrawl/search.json
127
-
128
- # Get titles and URLs
129
- jq -r '.data.web[] | "\(.title): \(.url)"' .firecrawl/search.json
130
- ```
131
-
132
- ## Parallelization
133
-
134
- Run independent operations in parallel. Check `firecrawl --status` for concurrency limit:
135
-
136
- ```bash
137
- firecrawl scrape "<url-1>" -o .firecrawl/1.md &
138
- firecrawl scrape "<url-2>" -o .firecrawl/2.md &
139
- firecrawl scrape "<url-3>" -o .firecrawl/3.md &
140
- wait
141
- ```
142
-
143
- For interact, scrape multiple pages and interact with each independently using their scrape IDs.
144
-
145
- ## Credit Usage
146
-
147
- ```bash
148
- firecrawl credit-usage
149
- firecrawl credit-usage --json --pretty -o .firecrawl/credits.json
150
- ```