@czottmann/pi-automode 1.1.0 → 1.2.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/extensions/auto-mode/classifier.ts +152 -0
- package/extensions/auto-mode/config.ts +399 -0
- package/extensions/auto-mode/constants.ts +168 -0
- package/extensions/auto-mode/extension.ts +402 -0
- package/extensions/auto-mode/hard-deny.ts +348 -0
- package/extensions/auto-mode/model-selector.ts +113 -0
- package/extensions/auto-mode/model.ts +13 -0
- package/extensions/auto-mode/paths.ts +134 -0
- package/extensions/auto-mode/permissions.ts +90 -0
- package/extensions/auto-mode/state.ts +103 -0
- package/extensions/auto-mode/transcript.ts +88 -0
- package/extensions/auto-mode/types.ts +95 -0
- package/extensions/auto-mode/utils.ts +46 -0
- package/extensions/auto-mode.ts +14 -1951
- package/package.json +2 -2
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { DENIAL_HISTORY_LIMIT } from "./constants.ts";
|
|
3
|
+
import type { AutoModeState, DenialRecord, EffectiveConfig } from "./types.ts";
|
|
4
|
+
import { safeJson, truncateMiddle } from "./utils.ts";
|
|
5
|
+
|
|
6
|
+
export function pushDenial(state: AutoModeState, denial: DenialRecord): void {
|
|
7
|
+
state.recentDenials = [
|
|
8
|
+
...state.recentDenials.slice(-(DENIAL_HISTORY_LIMIT - 1)),
|
|
9
|
+
denial,
|
|
10
|
+
];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function statusLine(
|
|
14
|
+
config: EffectiveConfig,
|
|
15
|
+
state: AutoModeState,
|
|
16
|
+
): string {
|
|
17
|
+
const enabled = state.enabledOverride ?? config.enabled;
|
|
18
|
+
if (!enabled) return "Auto-mode off";
|
|
19
|
+
let line = `Auto-mode on • checked: ${state.checkedActions}`;
|
|
20
|
+
if (state.blockedActions > 0) {
|
|
21
|
+
line =
|
|
22
|
+
`Auto-mode on · blocked: ${state.blockedActions}/${state.checkedActions}`;
|
|
23
|
+
const last = state.recentDenials.at(-1);
|
|
24
|
+
if (last) line += ` · last: ${truncateMiddle(last.reason, 60)}`;
|
|
25
|
+
}
|
|
26
|
+
return line;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function statusText(
|
|
30
|
+
config: EffectiveConfig,
|
|
31
|
+
state: AutoModeState,
|
|
32
|
+
): string {
|
|
33
|
+
return [
|
|
34
|
+
`enabled: ${(state.enabledOverride ?? config.enabled) ? "yes" : "no"}`,
|
|
35
|
+
`classifier: ${
|
|
36
|
+
state.classifierModelOverride ?? config.classifierModel ??
|
|
37
|
+
"current session model"
|
|
38
|
+
}`,
|
|
39
|
+
`checked actions: ${state.checkedActions}`,
|
|
40
|
+
`blocked actions: ${state.blockedActions}`,
|
|
41
|
+
`permissions.deny rules: ${config.permissionDeny.length}`,
|
|
42
|
+
`permissions.ask rules: ${config.permissionAsk.length}`,
|
|
43
|
+
`environment entries: ${config.environment.length}`,
|
|
44
|
+
`allow entries: ${config.allow.length}`,
|
|
45
|
+
`soft_deny entries: ${config.softDeny.length}`,
|
|
46
|
+
`hard_deny entries: ${config.hardDeny.length}`,
|
|
47
|
+
`last decision: ${state.lastDecision ?? "none"}`,
|
|
48
|
+
`last reason: ${state.lastReason ?? "none"}`,
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function formatDenials(state: AutoModeState): string {
|
|
53
|
+
if (state.recentDenials.length === 0) return "No recent auto-mode denials.";
|
|
54
|
+
return state.recentDenials
|
|
55
|
+
.slice()
|
|
56
|
+
.reverse()
|
|
57
|
+
.map(
|
|
58
|
+
(denial) =>
|
|
59
|
+
`${
|
|
60
|
+
new Date(denial.timestamp).toLocaleTimeString()
|
|
61
|
+
} ${denial.kind} ${denial.toolName}: ${denial.reason}\n ${
|
|
62
|
+
truncateMiddle(denial.action, 300)
|
|
63
|
+
}`,
|
|
64
|
+
)
|
|
65
|
+
.join("\n\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function actionSummary(
|
|
69
|
+
toolName: string,
|
|
70
|
+
input: Record<string, unknown>,
|
|
71
|
+
): string {
|
|
72
|
+
return `${toolName} ${safeJson(input, 6000)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function restoreState(ctx: ExtensionContext): AutoModeState {
|
|
76
|
+
const entries = ctx.sessionManager.getEntries();
|
|
77
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
78
|
+
const entry = entries[i] as {
|
|
79
|
+
type?: string;
|
|
80
|
+
customType?: string;
|
|
81
|
+
data?: Partial<AutoModeState>;
|
|
82
|
+
};
|
|
83
|
+
if (
|
|
84
|
+
entry.type !== "custom" ||
|
|
85
|
+
entry.customType !== "pi-automode-state" ||
|
|
86
|
+
!entry.data
|
|
87
|
+
) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
enabledOverride: entry.data.enabledOverride,
|
|
92
|
+
classifierModelOverride: entry.data.classifierModelOverride,
|
|
93
|
+
lastDecision: entry.data.lastDecision,
|
|
94
|
+
lastReason: entry.data.lastReason,
|
|
95
|
+
checkedActions: entry.data.checkedActions ?? 0,
|
|
96
|
+
blockedActions: entry.data.blockedActions ?? 0,
|
|
97
|
+
recentDenials: Array.isArray(entry.data.recentDenials)
|
|
98
|
+
? entry.data.recentDenials.slice(-DENIAL_HISTORY_LIMIT)
|
|
99
|
+
: [],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return { checkedActions: 0, blockedActions: 0, recentDenials: [] };
|
|
103
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { safeJson, truncateMiddle } from "./utils.ts";
|
|
3
|
+
|
|
4
|
+
function flattenUserContent(content: unknown): string {
|
|
5
|
+
if (typeof content === "string") return content;
|
|
6
|
+
if (!Array.isArray(content)) return "";
|
|
7
|
+
return content
|
|
8
|
+
.filter(
|
|
9
|
+
(block): block is { type: string; text?: string } =>
|
|
10
|
+
!!block && typeof block === "object" && "type" in block,
|
|
11
|
+
)
|
|
12
|
+
.filter((block) => block.type === "text" && typeof block.text === "string")
|
|
13
|
+
.map((block) => block.text ?? "")
|
|
14
|
+
.join("\n");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function flattenAssistantText(content: unknown): string {
|
|
18
|
+
if (!Array.isArray(content)) return "";
|
|
19
|
+
return content
|
|
20
|
+
.filter(
|
|
21
|
+
(block): block is { type: string; text?: string } =>
|
|
22
|
+
!!block && typeof block === "object" && "type" in block,
|
|
23
|
+
)
|
|
24
|
+
.filter((block) => block.type === "text" && typeof block.text === "string")
|
|
25
|
+
.map((block) => block.text ?? "")
|
|
26
|
+
.join("\n");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function collectAssistantToolCalls(content: unknown): string[] {
|
|
30
|
+
if (!Array.isArray(content)) return [];
|
|
31
|
+
return content
|
|
32
|
+
.filter(
|
|
33
|
+
(
|
|
34
|
+
block,
|
|
35
|
+
): block is {
|
|
36
|
+
type: string;
|
|
37
|
+
name?: string;
|
|
38
|
+
arguments?: unknown;
|
|
39
|
+
input?: unknown;
|
|
40
|
+
} => !!block && typeof block === "object" && "type" in block,
|
|
41
|
+
)
|
|
42
|
+
.filter((block) => block.type === "toolCall" || block.type === "tool_use")
|
|
43
|
+
.map(
|
|
44
|
+
(block) =>
|
|
45
|
+
`${String(block.name ?? "tool")} ${
|
|
46
|
+
safeJson("arguments" in block ? block.arguments : block.input, 1200)
|
|
47
|
+
}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildTranscript(
|
|
52
|
+
ctx: ExtensionContext,
|
|
53
|
+
maxLines: number,
|
|
54
|
+
): string {
|
|
55
|
+
const lines: string[] = [];
|
|
56
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
57
|
+
if (entry.type !== "message") continue;
|
|
58
|
+
const message = entry.message as { role?: string; content?: unknown };
|
|
59
|
+
if (message.role === "user") {
|
|
60
|
+
const text = flattenUserContent(message.content).trim();
|
|
61
|
+
if (text) lines.push(`User: ${truncateMiddle(text, 2000)}`);
|
|
62
|
+
} else if (message.role === "assistant") {
|
|
63
|
+
const text = flattenAssistantText(message.content).trim();
|
|
64
|
+
if (text) lines.push(`Assistant: ${truncateMiddle(text, 2000)}`);
|
|
65
|
+
for (const toolCall of collectAssistantToolCalls(message.content)) {
|
|
66
|
+
lines.push(`AssistantAction: ${toolCall}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return lines.slice(-maxLines).join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function loadedContextFromSystemPromptOptions(options: unknown): string {
|
|
74
|
+
const contextFiles = (
|
|
75
|
+
options as
|
|
76
|
+
| { contextFiles?: Array<{ path?: string; content?: string }> }
|
|
77
|
+
| undefined
|
|
78
|
+
)?.contextFiles;
|
|
79
|
+
if (!Array.isArray(contextFiles)) return "";
|
|
80
|
+
return contextFiles
|
|
81
|
+
.map(
|
|
82
|
+
(file) =>
|
|
83
|
+
`# ${file.path ?? "context"}\n${
|
|
84
|
+
truncateMiddle(file.content ?? "", 4000)
|
|
85
|
+
}`,
|
|
86
|
+
)
|
|
87
|
+
.join("\n\n");
|
|
88
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export type AutoModeSettings = {
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
classifierModel?: string;
|
|
6
|
+
maxTranscriptLines?: number;
|
|
7
|
+
environment?: unknown;
|
|
8
|
+
allow?: unknown;
|
|
9
|
+
protectedPaths?: unknown;
|
|
10
|
+
soft_deny?: unknown;
|
|
11
|
+
softDeny?: unknown;
|
|
12
|
+
hard_deny?: unknown;
|
|
13
|
+
hardDeny?: unknown;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type SettingsFile = {
|
|
17
|
+
autoMode?: AutoModeSettings;
|
|
18
|
+
permissions?: {
|
|
19
|
+
deny?: unknown;
|
|
20
|
+
ask?: unknown;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type LoadedSettingsFile = {
|
|
25
|
+
path: string;
|
|
26
|
+
settings?: SettingsFile;
|
|
27
|
+
diagnostics: string[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ToolPattern = {
|
|
31
|
+
raw: string;
|
|
32
|
+
toolName?: string;
|
|
33
|
+
argumentPattern?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type EffectiveConfig = {
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
classifierModel?: string;
|
|
39
|
+
maxTranscriptLines: number;
|
|
40
|
+
environment: string[];
|
|
41
|
+
allow: string[];
|
|
42
|
+
protectedPaths: string[];
|
|
43
|
+
softDeny: string[];
|
|
44
|
+
hardDeny: string[];
|
|
45
|
+
permissionDeny: ToolPattern[];
|
|
46
|
+
permissionAsk: ToolPattern[];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type AutoModeState = {
|
|
50
|
+
enabledOverride?: boolean;
|
|
51
|
+
classifierModelOverride?: string;
|
|
52
|
+
lastDecision?: "allow" | "block";
|
|
53
|
+
lastReason?: string;
|
|
54
|
+
checkedActions: number;
|
|
55
|
+
blockedActions: number;
|
|
56
|
+
recentDenials: DenialRecord[];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type DenialRecord = {
|
|
60
|
+
timestamp: number;
|
|
61
|
+
toolName: string;
|
|
62
|
+
reason: string;
|
|
63
|
+
action: string;
|
|
64
|
+
kind:
|
|
65
|
+
| "permissions.deny"
|
|
66
|
+
| "permissions.ask"
|
|
67
|
+
| "deterministic-hard-deny"
|
|
68
|
+
| "classifier"
|
|
69
|
+
| "setup";
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type ClassificationDecision = {
|
|
73
|
+
decision: "allow" | "block";
|
|
74
|
+
tier: "hard_deny" | "soft_deny" | "allow" | "explicit_intent" | "none";
|
|
75
|
+
reason: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type SettingsSources = {
|
|
79
|
+
globalSettings?: SettingsFile[];
|
|
80
|
+
projectLocalSettings?: SettingsFile[];
|
|
81
|
+
projectSharedSettings?: SettingsFile[];
|
|
82
|
+
inlineSettings?: SettingsFile[];
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type ConfigLoadResult = {
|
|
86
|
+
config: EffectiveConfig;
|
|
87
|
+
diagnostics: string[];
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export type ClassifyAction = (
|
|
91
|
+
ctx: ExtensionContext,
|
|
92
|
+
config: EffectiveConfig,
|
|
93
|
+
action: string,
|
|
94
|
+
loadedContext: string,
|
|
95
|
+
) => Promise<ClassificationDecision>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function stringArray(value: unknown): string[] | undefined {
|
|
2
|
+
if (!Array.isArray(value)) return undefined;
|
|
3
|
+
return value.filter(
|
|
4
|
+
(entry): entry is string =>
|
|
5
|
+
typeof entry === "string" && entry.trim().length > 0,
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function hasOwn(object: object, key: string): boolean {
|
|
10
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function truncateMiddle(text: string, maxLength: number): string {
|
|
14
|
+
if (text.length <= maxLength) return text;
|
|
15
|
+
const head = Math.floor(maxLength * 0.65);
|
|
16
|
+
const tail = maxLength - head - 18;
|
|
17
|
+
return `${text.slice(0, head)}… […] …${text.slice(text.length - tail)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function safeJson(value: unknown, maxLength = 4000): string {
|
|
21
|
+
const seen = new WeakSet<object>();
|
|
22
|
+
let text = "{}";
|
|
23
|
+
try {
|
|
24
|
+
text = JSON.stringify(
|
|
25
|
+
value,
|
|
26
|
+
(_key, current) => {
|
|
27
|
+
if (typeof current === "string") {
|
|
28
|
+
return truncateMiddle(
|
|
29
|
+
current,
|
|
30
|
+
Math.max(200, Math.floor(maxLength / 4)),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(current)) return current.slice(0, 30);
|
|
34
|
+
if (current && typeof current === "object") {
|
|
35
|
+
if (seen.has(current)) return "[Circular]";
|
|
36
|
+
seen.add(current);
|
|
37
|
+
}
|
|
38
|
+
return current;
|
|
39
|
+
},
|
|
40
|
+
2,
|
|
41
|
+
) ?? "{}";
|
|
42
|
+
} catch {
|
|
43
|
+
text = String(value);
|
|
44
|
+
}
|
|
45
|
+
return truncateMiddle(text, maxLength);
|
|
46
|
+
}
|