@ibartel74/pi-automode-ext 1.0.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.
@@ -0,0 +1,106 @@
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
+ const circle = enabled ? "●" : "○";
19
+ const allowed = state.checkedActions - state.blockedActions;
20
+ const classifier = state.classifierAllowed > 0 || state.classifierDenied > 0
21
+ ? ` ca:${state.classifierAllowed} cd:${state.classifierDenied}`
22
+ : "";
23
+ const confirmed = state.userConfirmed > 0 ? ` uc:${state.userConfirmed}` : "";
24
+ return `AM${circle} a:${allowed} d:${state.blockedActions}${classifier}${confirmed}`;
25
+ }
26
+
27
+ export function statusText(
28
+ config: EffectiveConfig,
29
+ state: AutoModeState,
30
+ ): string {
31
+ return [
32
+ `enabled: ${(state.enabledOverride ?? config.enabled) ? "yes" : "no"}`,
33
+ `classifier: ${config.classifierModel ?? "current session model"}`,
34
+ `classifier reasoning: ${config.classifierReasoningLevel ?? "server default"}`,
35
+ `interactive confirm: ${config.interactiveConfirm ? "on" : "off"}`,
36
+ `checked actions: ${state.checkedActions}`,
37
+ `blocked actions: ${state.blockedActions}`,
38
+ `classifier allowed: ${state.classifierAllowed}`,
39
+ `classifier denied: ${state.classifierDenied}`,
40
+ `user confirmed: ${state.userConfirmed}`,
41
+ `permissions.deny rules: ${config.permissionDeny.length}`,
42
+ `permissions.ask rules: ${config.permissionAsk.length}`,
43
+ `permissions.allow rules: ${config.permissionAllow.length}`,
44
+ `environment entries: ${config.environment.length}`,
45
+ `allow entries: ${config.allow.length}`,
46
+ `soft_deny entries: ${config.softDeny.length}`,
47
+ `hard_deny entries: ${config.hardDeny.length}`,
48
+ `last decision: ${state.lastDecision ?? "none"}`,
49
+ `last reason: ${state.lastReason ?? "none"}`,
50
+ ].join("\n");
51
+ }
52
+
53
+ export function formatDenials(state: AutoModeState): string {
54
+ if (state.recentDenials.length === 0) return "No recent auto-mode denials.";
55
+ return state.recentDenials
56
+ .slice()
57
+ .reverse()
58
+ .map(
59
+ (denial) =>
60
+ `${
61
+ new Date(denial.timestamp).toLocaleTimeString()
62
+ } ${denial.kind} ${denial.toolName}: ${denial.reason}\n ${
63
+ truncateMiddle(denial.action, 300)
64
+ }`,
65
+ )
66
+ .join("\n\n");
67
+ }
68
+
69
+ export function actionSummary(
70
+ toolName: string,
71
+ input: Record<string, unknown>,
72
+ ): string {
73
+ return `${toolName} ${safeJson(input, 6000)}`;
74
+ }
75
+
76
+ export function restoreState(ctx: ExtensionContext): AutoModeState {
77
+ const entries = ctx.sessionManager.getEntries();
78
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
79
+ const entry = entries[i] as {
80
+ type?: string;
81
+ customType?: string;
82
+ data?: Partial<AutoModeState>;
83
+ };
84
+ if (
85
+ entry.type !== "custom" ||
86
+ entry.customType !== "pi-automode-state" ||
87
+ !entry.data
88
+ ) {
89
+ continue;
90
+ }
91
+ return {
92
+ enabledOverride: entry.data.enabledOverride,
93
+ lastDecision: entry.data.lastDecision,
94
+ lastReason: entry.data.lastReason,
95
+ checkedActions: entry.data.checkedActions ?? 0,
96
+ blockedActions: entry.data.blockedActions ?? 0,
97
+ classifierAllowed: entry.data.classifierAllowed ?? 0,
98
+ classifierDenied: entry.data.classifierDenied ?? 0,
99
+ userConfirmed: entry.data.userConfirmed ?? 0,
100
+ recentDenials: Array.isArray(entry.data.recentDenials)
101
+ ? entry.data.recentDenials.slice(-DENIAL_HISTORY_LIMIT)
102
+ : [],
103
+ };
104
+ }
105
+ return { checkedActions: 0, blockedActions: 0, classifierAllowed: 0, classifierDenied: 0, userConfirmed: 0, recentDenials: [] };
106
+ }
@@ -0,0 +1,236 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { safeJson, truncateMiddle } from "./utils.ts";
3
+
4
+ const MAX_USER_ENTRY_TOKENS = 1000;
5
+ const MAX_TOOL_ENTRY_TOKENS = 1000;
6
+ const MAX_RECENT_TOOL_ENTRIES = 40;
7
+ const CHARS_PER_APPROX_TOKEN = 4;
8
+
9
+ type TranscriptEntry = {
10
+ index: number;
11
+ order: number;
12
+ kind: "user" | "tool";
13
+ text: string;
14
+ };
15
+
16
+ export type ClassifierTranscriptBudgets = {
17
+ maxUserTokens: number;
18
+ maxToolTokens: number;
19
+ };
20
+
21
+ function flattenUserContent(content: unknown): string {
22
+ if (typeof content === "string") return content;
23
+ if (!Array.isArray(content)) return "";
24
+ return content
25
+ .filter(
26
+ (block): block is { type: string; text?: string } =>
27
+ !!block && typeof block === "object" && "type" in block,
28
+ )
29
+ .filter((block) => block.type === "text" && typeof block.text === "string")
30
+ .map((block) => block.text ?? "")
31
+ .join("\n");
32
+ }
33
+
34
+ function collectAssistantToolCalls(content: unknown): Array<{
35
+ name: string;
36
+ input: unknown;
37
+ }> {
38
+ if (!Array.isArray(content)) return [];
39
+ return content
40
+ .filter(
41
+ (
42
+ block,
43
+ ): block is {
44
+ type: string;
45
+ name?: string;
46
+ arguments?: unknown;
47
+ input?: unknown;
48
+ } => !!block && typeof block === "object" && "type" in block,
49
+ )
50
+ .filter((block) => block.type === "toolCall" || block.type === "tool_use")
51
+ .map((block) => ({
52
+ name: String(block.name ?? "tool"),
53
+ input: "arguments" in block ? block.arguments : block.input,
54
+ }));
55
+ }
56
+
57
+ export function approximateTokenCount(text: string): number {
58
+ return Math.ceil(text.length / CHARS_PER_APPROX_TOKEN);
59
+ }
60
+
61
+ function truncateToTokenCap(
62
+ text: string,
63
+ maxTokens: number,
64
+ ): { text: string; truncated: boolean } {
65
+ if (approximateTokenCount(text) <= maxTokens) {
66
+ return { text, truncated: false };
67
+ }
68
+ const maxCharacters = Math.max(1, maxTokens * CHARS_PER_APPROX_TOKEN);
69
+ const omittedTokens = Math.max(
70
+ 1,
71
+ approximateTokenCount(text) - maxTokens,
72
+ );
73
+ const marker = `<truncated approx_tokens="${omittedTokens}" />`;
74
+ if (marker.length >= maxCharacters) {
75
+ return { text: marker.slice(0, maxCharacters), truncated: true };
76
+ }
77
+
78
+ const retainedCharacters = maxCharacters - marker.length;
79
+ const prefixCharacters = Math.ceil(retainedCharacters * 0.65);
80
+ const suffixCharacters = retainedCharacters - prefixCharacters;
81
+ return {
82
+ text: `${text.slice(0, prefixCharacters)}${marker}${
83
+ suffixCharacters > 0 ? text.slice(-suffixCharacters) : ""
84
+ }`,
85
+ truncated: true,
86
+ };
87
+ }
88
+
89
+ function collectTranscriptEntries(ctx: ExtensionContext): TranscriptEntry[] {
90
+ const entries: TranscriptEntry[] = [];
91
+ const sessionManager = ctx.sessionManager as typeof ctx.sessionManager & {
92
+ buildContextEntries?: () => ReturnType<typeof ctx.sessionManager.getBranch>;
93
+ };
94
+ const contextEntries = sessionManager.buildContextEntries?.() ??
95
+ sessionManager.getBranch();
96
+
97
+ for (const [index, entry] of contextEntries.entries()) {
98
+ if (entry.type !== "message") continue;
99
+ const message = entry.message as { role?: string; content?: unknown };
100
+ if (message.role === "user") {
101
+ const text = flattenUserContent(message.content).trim();
102
+ if (text) entries.push({ index, order: 0, kind: "user", text });
103
+ continue;
104
+ }
105
+ if (message.role !== "assistant") continue;
106
+
107
+ for (const [order, toolCall] of collectAssistantToolCalls(
108
+ message.content,
109
+ ).entries()) {
110
+ entries.push({
111
+ index,
112
+ order,
113
+ kind: "tool",
114
+ text: `${toolCall.name}: ${safeJson(toolCall.input, 8000)}`,
115
+ });
116
+ }
117
+ }
118
+
119
+ return entries;
120
+ }
121
+
122
+ function selectUserEntries(
123
+ entries: TranscriptEntry[],
124
+ maxTokens: number,
125
+ ): { selected: TranscriptEntry[]; omitted: boolean } {
126
+ const users = entries.filter((entry) => entry.kind === "user");
127
+ if (users.length === 0) return { selected: [], omitted: false };
128
+
129
+ const distinctAnchors = users.length > 1;
130
+ const anchorBudget = distinctAnchors
131
+ ? Math.max(1, Math.floor(maxTokens / 2))
132
+ : maxTokens;
133
+ const entryCap = Math.min(MAX_USER_ENTRY_TOKENS, anchorBudget);
134
+ const rendered = users.map((entry) => {
135
+ const truncated = truncateToTokenCap(`User: ${entry.text}`, entryCap);
136
+ return { ...entry, text: truncated.text, truncated: truncated.truncated };
137
+ });
138
+ const selectedIndices = new Set<number>();
139
+ let usedTokens = 0;
140
+
141
+ const include = (index: number): void => {
142
+ if (selectedIndices.has(index)) return;
143
+ const entry = rendered[index];
144
+ if (!entry) return;
145
+ const tokens = approximateTokenCount(entry.text);
146
+ if (usedTokens + tokens > maxTokens) return;
147
+ selectedIndices.add(index);
148
+ usedTokens += tokens;
149
+ };
150
+
151
+ // Prefer the latest user instruction when an extremely small configured
152
+ // budget cannot retain both intent anchors.
153
+ include(rendered.length - 1);
154
+ include(0);
155
+ for (let index = rendered.length - 2; index > 0; index -= 1) {
156
+ include(index);
157
+ }
158
+
159
+ return {
160
+ selected: rendered.filter((_entry, index) => selectedIndices.has(index)),
161
+ omitted: selectedIndices.size < users.length || rendered.some((entry) =>
162
+ entry.truncated
163
+ ),
164
+ };
165
+ }
166
+
167
+ function selectToolEntries(
168
+ entries: TranscriptEntry[],
169
+ maxTokens: number,
170
+ ): { selected: TranscriptEntry[]; omitted: boolean } {
171
+ const tools = entries.filter((entry) => entry.kind === "tool");
172
+ const selected: TranscriptEntry[] = [];
173
+ let usedTokens = 0;
174
+
175
+ for (let index = tools.length - 1; index >= 0; index -= 1) {
176
+ if (selected.length >= MAX_RECENT_TOOL_ENTRIES) break;
177
+ const entry = tools[index];
178
+ if (!entry) continue;
179
+ const truncated = truncateToTokenCap(
180
+ `ToolCall ${entry.text}`,
181
+ Math.min(MAX_TOOL_ENTRY_TOKENS, maxTokens),
182
+ );
183
+ const tokens = approximateTokenCount(truncated.text);
184
+ if (usedTokens + tokens > maxTokens) continue;
185
+ selected.push({ ...entry, text: truncated.text });
186
+ usedTokens += tokens;
187
+ }
188
+
189
+ selected.reverse();
190
+ return {
191
+ selected,
192
+ omitted: selected.length < tools.length || tools.some((entry) =>
193
+ approximateTokenCount(`ToolCall ${entry.text}`) >
194
+ Math.min(MAX_TOOL_ENTRY_TOKENS, maxTokens)
195
+ ),
196
+ };
197
+ }
198
+
199
+ /** Build classifier evidence from user text and assistant tool-call payloads only. */
200
+ export function buildClassifierTranscript(
201
+ ctx: ExtensionContext,
202
+ budgets: ClassifierTranscriptBudgets,
203
+ ): string {
204
+ const entries = collectTranscriptEntries(ctx);
205
+ const users = selectUserEntries(entries, budgets.maxUserTokens);
206
+ const tools = selectToolEntries(entries, budgets.maxToolTokens);
207
+ const selected = [...users.selected, ...tools.selected].sort(
208
+ (left, right) => left.index - right.index || left.order - right.order,
209
+ );
210
+ if (users.omitted || tools.omitted) {
211
+ selected.push({
212
+ index: Number.MAX_SAFE_INTEGER,
213
+ order: 0,
214
+ kind: "tool",
215
+ text: "<transcript_entries_omitted />",
216
+ });
217
+ }
218
+ return selected.map((entry) => entry.text).join("\n");
219
+ }
220
+
221
+ export function loadedContextFromSystemPromptOptions(options: unknown): string {
222
+ const contextFiles = (
223
+ options as
224
+ | { contextFiles?: Array<{ path?: string; content?: string }> }
225
+ | undefined
226
+ )?.contextFiles;
227
+ if (!Array.isArray(contextFiles)) return "";
228
+ return contextFiles
229
+ .map(
230
+ (file) =>
231
+ `# ${file.path ?? "context"}\n${
232
+ truncateMiddle(file.content ?? "", 4000)
233
+ }`,
234
+ )
235
+ .join("\n\n");
236
+ }
@@ -0,0 +1,210 @@
1
+ import type { AssistantMessage } from "@earendil-works/pi-ai";
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+
4
+ export type ClassifierReasoningLevel =
5
+ | "low"
6
+ | "medium"
7
+ | "high"
8
+ | "xhigh"
9
+ | "max";
10
+
11
+ export type EffectiveClassifierReasoningLevel =
12
+ | "off"
13
+ | "minimal"
14
+ | ClassifierReasoningLevel;
15
+
16
+ export type ClassifierReasoning =
17
+ | { mode: "server-default" }
18
+ | {
19
+ mode: "explicit";
20
+ requestedLevel: ClassifierReasoningLevel;
21
+ effectiveLevel: EffectiveClassifierReasoningLevel;
22
+ };
23
+
24
+ export type ClassifierReasoningLog =
25
+ | ClassifierReasoning
26
+ | {
27
+ mode: "explicit";
28
+ requestedLevel: ClassifierReasoningLevel;
29
+ effectiveLevel?: undefined;
30
+ };
31
+
32
+ /** Observability log configuration. Off by default. */
33
+ export type LogConfig = {
34
+ enabled: boolean;
35
+ /** When true, also log classifier prompt/response payloads. */
36
+ classifierIo: boolean;
37
+ };
38
+
39
+ export type AutoModeSettings = {
40
+ enabled?: boolean;
41
+ classifierModel?: string;
42
+ classifierReasoningLevel?: ClassifierReasoningLevel;
43
+ /** When true, read-only tools (read/grep/find/ls) are classified instead of auto-allowed. */
44
+ classifyReadOnlyTools?: boolean;
45
+ /** Override the fast-stage completion token budget (default 512). */
46
+ fastClassifierMaxTokens?: number;
47
+ /** Per-request timeout for classifier completions in milliseconds (default 20000). */
48
+ classifierTimeoutMs?: number;
49
+ /** When true, file tools whose resolved path is inside the working directory are allowed deterministically (no classifier), and outside-CWD file access is classified. */
50
+ allowInsideWorkingDirectory?: boolean;
51
+ /** When true (default), a classifier block prompts for interactive user confirmation instead of blocking outright, when a UI is available. */
52
+ interactiveConfirm?: boolean;
53
+ /** Path glob patterns (file tools) that are always denied before the classifier. Supports `~` and `*` (matches any characters, including `/`). */
54
+ deniedPaths?: unknown;
55
+ maxUserTranscriptTokens?: number;
56
+ maxToolTranscriptTokens?: number;
57
+ environment?: unknown;
58
+ allow?: unknown;
59
+ protectedPaths?: unknown;
60
+ soft_deny?: unknown;
61
+ softDeny?: unknown;
62
+ hard_deny?: unknown;
63
+ hardDeny?: unknown;
64
+ log?: Partial<LogConfig>;
65
+ };
66
+
67
+ export type SettingsFile = {
68
+ autoMode?: AutoModeSettings;
69
+ permissions?: {
70
+ deny?: unknown;
71
+ ask?: unknown;
72
+ /**
73
+ * Deterministic allow tier: matching calls skip the classifier only. Read
74
+ * from user-owned config sources, never shared project config.
75
+ */
76
+ allow?: unknown;
77
+ };
78
+ };
79
+
80
+ export type LoadedSettingsFile = {
81
+ path: string;
82
+ settings?: SettingsFile;
83
+ diagnostics: string[];
84
+ };
85
+
86
+ export type ToolPattern = {
87
+ raw: string;
88
+ toolName?: string;
89
+ argumentPattern?: string;
90
+ };
91
+
92
+ export type EffectiveConfig = {
93
+ enabled: boolean;
94
+ classifierModel?: string;
95
+ classifierReasoningLevel?: ClassifierReasoningLevel;
96
+ classifyReadOnlyTools: boolean;
97
+ fastClassifierMaxTokens: number;
98
+ classifierTimeoutMs: number;
99
+ allowInsideWorkingDirectory: boolean;
100
+ interactiveConfirm: boolean;
101
+ deniedPaths: string[];
102
+ maxUserTranscriptTokens: number;
103
+ maxToolTranscriptTokens: number;
104
+ environment: string[];
105
+ allow: string[];
106
+ protectedPaths: string[];
107
+ softDeny: string[];
108
+ hardDeny: string[];
109
+ permissionDeny: ToolPattern[];
110
+ permissionAsk: ToolPattern[];
111
+ permissionAllow: ToolPattern[];
112
+ log: LogConfig;
113
+ };
114
+
115
+ export type AutoModeState = {
116
+ enabledOverride?: boolean;
117
+ lastDecision?: "allow" | "block";
118
+ lastReason?: string;
119
+ checkedActions: number;
120
+ blockedActions: number;
121
+ classifierAllowed: number;
122
+ classifierDenied: number;
123
+ userConfirmed: number;
124
+ recentDenials: DenialRecord[];
125
+ };
126
+
127
+ export type DenialRecord = {
128
+ timestamp: number;
129
+ toolName: string;
130
+ reason: string;
131
+ action: string;
132
+ kind:
133
+ | "permissions.deny"
134
+ | "permissions.ask"
135
+ | "deterministic-hard-deny"
136
+ | "deterministic-path-deny"
137
+ | "classifier"
138
+ | "setup";
139
+ };
140
+
141
+ /** Denial kind plus the deterministic allow fast paths, used for decision log entries. */
142
+ export type DecisionKind =
143
+ | DenialRecord["kind"]
144
+ | "permissions.allow"
145
+ | "read-only"
146
+ | "inside-working-directory"
147
+ | "user-confirmed";
148
+
149
+ export type ClassificationDecision = {
150
+ decision: "allow" | "block";
151
+ tier: "hard_deny" | "soft_deny" | "allow" | "explicit_intent" | "none";
152
+ reason: string;
153
+ };
154
+
155
+ /** One classifier attempt: the raw model response (or error) and parsed decision. */
156
+ export type ClassifierIoAttempt = {
157
+ stage: "fast" | "detailed";
158
+ attempt: number;
159
+ response?: {
160
+ stopReason?: string;
161
+ text: string;
162
+ model: string;
163
+ timestamp: number;
164
+ usage: AssistantMessage["usage"];
165
+ errorMessage?: string;
166
+ };
167
+ parsed?: ClassificationDecision;
168
+ error?: string;
169
+ durationMs: number;
170
+ };
171
+
172
+ /** Full classifier I/O for an action, surfaced for optional observability logging. */
173
+ export type ClassifierIo = {
174
+ model: string;
175
+ reasoning: ClassifierReasoning;
176
+ prompt: {
177
+ system: string;
178
+ context: string;
179
+ action: string;
180
+ fastInstruction: string;
181
+ detailedInstruction: string;
182
+ };
183
+ attempts: ClassifierIoAttempt[];
184
+ durationMs: number;
185
+ };
186
+
187
+ /** Classification decision plus resolved reasoning and the I/O that produced it (when available). */
188
+ export type ClassifyResult = ClassificationDecision & {
189
+ reasoning?: ClassifierReasoningLog;
190
+ io?: ClassifierIo;
191
+ };
192
+
193
+ export type SettingsSources = {
194
+ globalSettings?: SettingsFile[];
195
+ projectLocalSettings?: SettingsFile[];
196
+ projectSharedSettings?: SettingsFile[];
197
+ inlineSettings?: SettingsFile[];
198
+ };
199
+
200
+ export type ConfigLoadResult = {
201
+ config: EffectiveConfig;
202
+ diagnostics: string[];
203
+ };
204
+
205
+ export type ClassifyAction = (
206
+ ctx: ExtensionContext,
207
+ config: EffectiveConfig,
208
+ action: string,
209
+ loadedContext: string,
210
+ ) => Promise<ClassifyResult>;
@@ -0,0 +1,54 @@
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)) {
34
+ if (current.length <= 30) return current;
35
+ return {
36
+ $truncatedArray: true,
37
+ items: current.slice(0, 30),
38
+ omittedEntries: current.length - 30,
39
+ totalEntries: current.length,
40
+ };
41
+ }
42
+ if (current && typeof current === "object") {
43
+ if (seen.has(current)) return "[Circular]";
44
+ seen.add(current);
45
+ }
46
+ return current;
47
+ },
48
+ 2,
49
+ ) ?? "{}";
50
+ } catch {
51
+ text = String(value);
52
+ }
53
+ return truncateMiddle(text, maxLength);
54
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Claude Code-style auto mode for Pi.
3
+ *
4
+ * The enforcement order is deliberately different from simple "auto reviewer" plugins:
5
+ * permission deny/ask rules and deterministic hard-deny checks run before any fast-path allow.
6
+ * Only read-only built-in tools bypass classification; every side-effecting action goes through the classifier.
7
+ */
8
+
9
+ export * from "./auto-mode/classifier.ts";
10
+ export * from "./auto-mode/bash.ts";
11
+ export * from "./auto-mode/config.ts";
12
+ export * from "./auto-mode/constants.ts";
13
+ export * from "./auto-mode/extension.ts";
14
+ export * from "./auto-mode/hard-deny.ts";
15
+ export * from "./auto-mode/jev.ts";
16
+ export * from "./auto-mode/log.ts";
17
+ export * from "./auto-mode/model.ts";
18
+ export * from "./auto-mode/model-selector.ts";
19
+ export * from "./auto-mode/paths.ts";
20
+ export * from "./auto-mode/permissions.ts";
21
+ export * from "./auto-mode/state.ts";
22
+ export * from "./auto-mode/transcript.ts";
23
+ export * from "./auto-mode/types.ts";
24
+
25
+ import { createPiAutomode } from "./auto-mode/extension.ts";
26
+
27
+ export default createPiAutomode();
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@ibartel74/pi-automode-ext",
3
+ "version": "1.0.0",
4
+ "description": "Claude Code-style auto mode guardrail for pi.",
5
+ "repository": {
6
+ "url": "https://github.com/ibartel/pi-automode-ext"
7
+ },
8
+ "type": "module",
9
+ "license": "MIT",
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "keywords": [
14
+ "pi",
15
+ "pi-extension",
16
+ "pi-package",
17
+ "auto-mode",
18
+ "llm",
19
+ "coding-agent"
20
+ ],
21
+ "scripts": {
22
+ "typecheck": "tsc --noEmit",
23
+ "check": "tsc --noEmit",
24
+ "build": "tsc --noEmit",
25
+ "test": "node --import tsx --test tests/*.test.ts"
26
+ },
27
+ "files": [
28
+ "CHANGELOG.md",
29
+ "docs",
30
+ "extensions",
31
+ "examples",
32
+ "skills",
33
+ "README.md"
34
+ ],
35
+ "pi": {
36
+ "extensions": [
37
+ "./extensions/auto-mode.ts"
38
+ ],
39
+ "skills": [
40
+ "./skills"
41
+ ]
42
+ },
43
+ "peerDependencies": {
44
+ "@earendil-works/pi-ai": "*",
45
+ "@earendil-works/pi-coding-agent": "*",
46
+ "@earendil-works/pi-tui": "*",
47
+ "typebox": "*"
48
+ },
49
+ "devDependencies": {
50
+ "@earendil-works/pi-ai": "^0.84.1",
51
+ "@earendil-works/pi-coding-agent": "^0.84.1",
52
+ "@earendil-works/pi-tui": "^0.84.1",
53
+ "@types/node": "^24.0.0",
54
+ "tsx": "^4.22.4",
55
+ "typebox": "^1.3.10",
56
+ "typescript": "^5.8.0"
57
+ },
58
+ "dependencies": {
59
+ "unbash": "4.0.10"
60
+ }
61
+ }