oh-pi 0.1.70 β 0.1.72
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/dist/i18n.test.d.ts +1 -0
- package/dist/i18n.test.js +56 -0
- package/dist/registry.test.d.ts +1 -0
- package/dist/registry.test.js +68 -0
- package/dist/tui/confirm-apply.d.ts +7 -0
- package/dist/tui/confirm-apply.js +1 -1
- package/dist/tui/confirm-apply.test.d.ts +1 -0
- package/dist/tui/confirm-apply.test.js +20 -0
- package/dist/tui/provider-setup.d.ts +2 -0
- package/dist/tui/provider-setup.js +1 -1
- package/dist/tui/provider-setup.test.d.ts +1 -0
- package/dist/tui/provider-setup.test.js +40 -0
- package/dist/tui/welcome.d.ts +6 -0
- package/dist/tui/welcome.js +1 -1
- package/dist/tui/welcome.test.d.ts +1 -0
- package/dist/tui/welcome.test.js +25 -0
- package/dist/utils/resources.test.d.ts +1 -0
- package/dist/utils/resources.test.js +39 -0
- package/package.json +5 -3
- package/pi-package/extensions/ant-colony/concurrency.test.ts +70 -0
- package/pi-package/extensions/ant-colony/deps.test.ts +62 -0
- package/pi-package/extensions/ant-colony/index.ts +4 -0
- package/pi-package/extensions/ant-colony/nest.ts +37 -44
- package/pi-package/extensions/ant-colony/parser.test.ts +110 -0
- package/pi-package/extensions/ant-colony/prompts.test.ts +57 -0
- package/pi-package/extensions/ant-colony/prompts.ts +7 -1
- package/pi-package/extensions/ant-colony/queen.ts +159 -21
- package/pi-package/extensions/ant-colony/spawner.test.ts +44 -0
- package/pi-package/extensions/ant-colony/spawner.ts +16 -1
- package/pi-package/extensions/ant-colony/types.test.ts +36 -0
- package/pi-package/extensions/ant-colony/ui.test.ts +66 -0
- package/pi-package/extensions/auto-update.test.ts +15 -0
- package/pi-package/extensions/auto-update.ts +1 -1
- package/pi-package/extensions/safe-guard.test.ts +26 -0
- package/pi-package/extensions/safe-guard.ts +2 -2
- package/pi-package/extensions/smart-compact.test.ts +64 -0
- package/pi-package/extensions/smart-compact.ts +2 -2
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { formatDuration, formatCost, formatTokens, statusIcon, casteIcon, buildReport } from "./ui.js";
|
|
3
|
+
import type { ColonyState } from "./types.js";
|
|
4
|
+
|
|
5
|
+
describe("formatDuration", () => {
|
|
6
|
+
it("0ms", () => expect(formatDuration(0)).toBe("0s"));
|
|
7
|
+
it("5000ms", () => expect(formatDuration(5000)).toBe("5s"));
|
|
8
|
+
it("59000ms", () => expect(formatDuration(59000)).toBe("59s"));
|
|
9
|
+
it("60000ms", () => expect(formatDuration(60000)).toBe("1m0s"));
|
|
10
|
+
it("90000ms", () => expect(formatDuration(90000)).toBe("1m30s"));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("formatCost", () => {
|
|
14
|
+
it("0.001", () => expect(formatCost(0.001)).toBe("$0.0010"));
|
|
15
|
+
it("0.009", () => expect(formatCost(0.009)).toBe("$0.0090"));
|
|
16
|
+
it("0.01", () => expect(formatCost(0.01)).toBe("$0.01"));
|
|
17
|
+
it("1.5", () => expect(formatCost(1.5)).toBe("$1.50"));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("formatTokens", () => {
|
|
21
|
+
it("500", () => expect(formatTokens(500)).toBe("500"));
|
|
22
|
+
it("999", () => expect(formatTokens(999)).toBe("999"));
|
|
23
|
+
it("1500", () => expect(formatTokens(1500)).toBe("1.5k"));
|
|
24
|
+
it("1500000", () => expect(formatTokens(1500000)).toBe("1.5M"));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("statusIcon", () => {
|
|
28
|
+
it("scouting", () => expect(statusIcon("scouting")).toBe("π"));
|
|
29
|
+
it("working", () => expect(statusIcon("working")).toBe("βοΈ"));
|
|
30
|
+
it("reviewing", () => expect(statusIcon("reviewing")).toBe("π‘οΈ"));
|
|
31
|
+
it("done", () => expect(statusIcon("done")).toBe("β
"));
|
|
32
|
+
it("failed", () => expect(statusIcon("failed")).toBe("β"));
|
|
33
|
+
it("budget_exceeded", () => expect(statusIcon("budget_exceeded")).toBe("π°"));
|
|
34
|
+
it("unknown", () => expect(statusIcon("xyz")).toBe("π"));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("casteIcon", () => {
|
|
38
|
+
it("scout", () => expect(casteIcon("scout")).toBe("π"));
|
|
39
|
+
it("soldier", () => expect(casteIcon("soldier")).toBe("π‘οΈ"));
|
|
40
|
+
it("drone", () => expect(casteIcon("drone")).toBe("βοΈ"));
|
|
41
|
+
it("worker", () => expect(casteIcon("worker")).toBe("βοΈ"));
|
|
42
|
+
it("unknown", () => expect(casteIcon("xyz")).toBe("βοΈ"));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("buildReport", () => {
|
|
46
|
+
it("builds report with goal, status, cost, tasks", () => {
|
|
47
|
+
const state: ColonyState = {
|
|
48
|
+
id: "c-1", goal: "Test goal", status: "done",
|
|
49
|
+
tasks: [
|
|
50
|
+
{ id: "t1", parentId: null, title: "Task A", description: "", caste: "worker", status: "done", priority: 3, files: [], claimedBy: null, result: null, error: null, spawnedTasks: [], createdAt: 0, startedAt: 0, finishedAt: 1000 },
|
|
51
|
+
{ id: "t2", parentId: null, title: "Task B", description: "", caste: "worker", status: "failed", priority: 3, files: [], claimedBy: null, result: null, error: "some error", spawnedTasks: [], createdAt: 0, startedAt: 0, finishedAt: 1000 },
|
|
52
|
+
],
|
|
53
|
+
ants: [], pheromones: [],
|
|
54
|
+
concurrency: { current: 2, min: 1, max: 4, optimal: 3, history: [] },
|
|
55
|
+
metrics: { tasksTotal: 2, tasksDone: 1, tasksFailed: 1, antsSpawned: 2, totalCost: 0.05, totalTokens: 1000, startTime: 0, throughputHistory: [] },
|
|
56
|
+
maxCost: null, modelOverrides: {}, createdAt: 0, finishedAt: 5000,
|
|
57
|
+
};
|
|
58
|
+
const report = buildReport(state);
|
|
59
|
+
expect(report).toContain("Test goal");
|
|
60
|
+
expect(report).toContain("β
");
|
|
61
|
+
expect(report).toContain("$0.05");
|
|
62
|
+
expect(report).toContain("Task A");
|
|
63
|
+
expect(report).toContain("Task B");
|
|
64
|
+
expect(report).toContain("some error");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("@mariozechner/pi-coding-agent", () => ({}));
|
|
4
|
+
|
|
5
|
+
import { isNewer } from "./auto-update";
|
|
6
|
+
|
|
7
|
+
describe("isNewer", () => {
|
|
8
|
+
it("patch newer", () => expect(isNewer("1.0.1", "1.0.0")).toBe(true));
|
|
9
|
+
it("minor newer", () => expect(isNewer("1.1.0", "1.0.0")).toBe(true));
|
|
10
|
+
it("major newer", () => expect(isNewer("2.0.0", "1.9.9")).toBe(true));
|
|
11
|
+
it("equal", () => expect(isNewer("1.0.0", "1.0.0")).toBe(false));
|
|
12
|
+
it("older", () => expect(isNewer("1.0.0", "1.0.1")).toBe(false));
|
|
13
|
+
it("0.1.70 > 0.1.69", () => expect(isNewer("0.1.70", "0.1.69")).toBe(true));
|
|
14
|
+
it("0.1.69 < 0.1.70", () => expect(isNewer("0.1.69", "0.1.70")).toBe(false));
|
|
15
|
+
});
|
|
@@ -39,7 +39,7 @@ function getCurrentVersion(): string | null {
|
|
|
39
39
|
} catch { return null; }
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function isNewer(latest: string, current: string): boolean {
|
|
42
|
+
export function isNewer(latest: string, current: string): boolean {
|
|
43
43
|
const a = latest.split(".").map(Number);
|
|
44
44
|
const b = current.split(".").map(Number);
|
|
45
45
|
for (let i = 0; i < 3; i++) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { DANGEROUS_PATTERNS, PROTECTED_PATHS } from "./safe-guard";
|
|
3
|
+
|
|
4
|
+
describe("DANGEROUS_PATTERNS", () => {
|
|
5
|
+
const matches = (cmd: string) => DANGEROUS_PATTERNS.some((p) => p.test(cmd));
|
|
6
|
+
|
|
7
|
+
it("matches rm -rf /", () => expect(matches("rm -rf /")).toBe(true));
|
|
8
|
+
it("matches rm -f file.txt", () => expect(matches("rm -f file.txt")).toBe(true));
|
|
9
|
+
it("matches rm --force file", () => expect(matches("rm --force file")).toBe(true));
|
|
10
|
+
it("matches sudo rm file", () => expect(matches("sudo rm file")).toBe(true));
|
|
11
|
+
it("matches DROP TABLE users", () => expect(matches("DROP TABLE users")).toBe(true));
|
|
12
|
+
it("matches TRUNCATE TABLE foo", () => expect(matches("TRUNCATE TABLE foo")).toBe(true));
|
|
13
|
+
it("matches DELETE FROM users", () => expect(matches("DELETE FROM users")).toBe(true));
|
|
14
|
+
it("matches chmod 777 /tmp", () => expect(matches("chmod 777 /tmp")).toBe(true));
|
|
15
|
+
it("matches mkfs /dev/sda", () => expect(matches("mkfs /dev/sda")).toBe(true));
|
|
16
|
+
it("matches dd if=/dev/zero", () => expect(matches("dd if=/dev/zero")).toBe(true));
|
|
17
|
+
it("does not match ls -la", () => expect(matches("ls -la")).toBe(false));
|
|
18
|
+
it("does not match echo hello", () => expect(matches("echo hello")).toBe(false));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("PROTECTED_PATHS", () => {
|
|
22
|
+
it("contains all expected paths", () => {
|
|
23
|
+
expect(PROTECTED_PATHS).toEqual([".env", ".git/", "node_modules/", ".pi/", "id_rsa", ".ssh/"]);
|
|
24
|
+
});
|
|
25
|
+
it("has length 6", () => expect(PROTECTED_PATHS).toHaveLength(6));
|
|
26
|
+
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
7
7
|
|
|
8
|
-
const DANGEROUS_PATTERNS = [
|
|
8
|
+
export const DANGEROUS_PATTERNS = [
|
|
9
9
|
/\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+|.*-rf\b|.*--force\b)/,
|
|
10
10
|
/\bsudo\s+rm\b/,
|
|
11
11
|
/\b(DROP|TRUNCATE|DELETE\s+FROM)\b/i,
|
|
@@ -15,7 +15,7 @@ const DANGEROUS_PATTERNS = [
|
|
|
15
15
|
/>\s*\/dev\/sd[a-z]/,
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
-
const PROTECTED_PATHS = [".env", ".git/", "node_modules/", ".pi/", "id_rsa", ".ssh/"];
|
|
18
|
+
export const PROTECTED_PATHS = [".env", ".git/", "node_modules/", ".pi/", "id_rsa", ".ssh/"];
|
|
19
19
|
|
|
20
20
|
export default function (pi: ExtensionAPI) {
|
|
21
21
|
pi.on("tool_call", async (event, ctx) => {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { truncateText, compactContent } from "./smart-compact";
|
|
3
|
+
|
|
4
|
+
const longMultiline = (lines: number, lineLen = 100) =>
|
|
5
|
+
Array.from({ length: lines }, (_, i) => "x".repeat(lineLen) + i).join("\n");
|
|
6
|
+
|
|
7
|
+
describe("truncateText", () => {
|
|
8
|
+
it("short text returns unchanged", () => {
|
|
9
|
+
expect(truncateText("hello", 8000)).toBe("hello");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("few lines but long chars returns unchanged", () => {
|
|
13
|
+
const text = "x".repeat(9000) + "\n" + "y".repeat(9000);
|
|
14
|
+
expect(truncateText(text, 8000)).toBe(text);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("long text with many lines gets truncated", () => {
|
|
18
|
+
const text = longMultiline(200);
|
|
19
|
+
const result = truncateText(text, 8000);
|
|
20
|
+
expect(result).toContain("[...truncated");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("preserves head and tail", () => {
|
|
24
|
+
const text = longMultiline(200);
|
|
25
|
+
const result = truncateText(text, 8000);
|
|
26
|
+
expect(result.startsWith(text.slice(0, 1500))).toBe(true);
|
|
27
|
+
expect(result.endsWith(text.slice(-2500))).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("custom head/tail params work", () => {
|
|
31
|
+
const text = longMultiline(200);
|
|
32
|
+
const result = truncateText(text, 100, 50, 50);
|
|
33
|
+
expect(result).toContain("[...truncated");
|
|
34
|
+
expect(result.startsWith(text.slice(0, 50))).toBe(true);
|
|
35
|
+
expect(result.endsWith(text.slice(-50))).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("compactContent", () => {
|
|
40
|
+
it("short string returns unchanged", () => {
|
|
41
|
+
expect(compactContent("short")).toBe("short");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("long string gets truncated", () => {
|
|
45
|
+
const text = longMultiline(200);
|
|
46
|
+
expect(compactContent(text)).toContain("[...truncated");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("array text block gets truncated", () => {
|
|
50
|
+
const text = longMultiline(200);
|
|
51
|
+
const result = compactContent([{ type: "text", text }]);
|
|
52
|
+
expect(result[0].text).toContain("[...truncated");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("array non-text block returned unchanged", () => {
|
|
56
|
+
const block = { type: "image", url: "x" };
|
|
57
|
+
expect(compactContent([block])).toEqual([block]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("non-array non-string returned as-is", () => {
|
|
61
|
+
expect(compactContent(42)).toBe(42);
|
|
62
|
+
expect(compactContent({ a: 1 })).toEqual({ a: 1 });
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -12,7 +12,7 @@ const MAX_USER_BLOCK_CHARS = 12000;
|
|
|
12
12
|
const KEEP_HEAD = 1500;
|
|
13
13
|
const KEEP_TAIL = 2500;
|
|
14
14
|
|
|
15
|
-
function truncateText(text: string, max: number, head = KEEP_HEAD, tail = KEEP_TAIL): string {
|
|
15
|
+
export function truncateText(text: string, max: number, head = KEEP_HEAD, tail = KEEP_TAIL): string {
|
|
16
16
|
if (text.length <= max) return text;
|
|
17
17
|
const lines = text.split("\n");
|
|
18
18
|
if (lines.length <= 10) return text; // ηζζ¬δΈθ£
|
|
@@ -22,7 +22,7 @@ function truncateText(text: string, max: number, head = KEEP_HEAD, tail = KEEP_T
|
|
|
22
22
|
return `${headText}\n\n[...truncated ${removedLines} lines...]\n\n${tailText}`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function compactContent(content: any): any {
|
|
25
|
+
export function compactContent(content: any): any {
|
|
26
26
|
if (typeof content === "string") {
|
|
27
27
|
return truncateText(content, MAX_TOOL_OUTPUT_CHARS);
|
|
28
28
|
}
|