pi-var 1.0.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 (64) hide show
  1. package/.github/FUNDING.yml +4 -0
  2. package/.prettierrc +7 -0
  3. package/README.md +240 -0
  4. package/commitlint.config.cjs +3 -0
  5. package/extensions/pi-var/__tests__/integration/smoke.test.ts +308 -0
  6. package/extensions/pi-var/__tests__/unit/command.test.ts +322 -0
  7. package/extensions/pi-var/__tests__/unit/cow-detection.test.ts +340 -0
  8. package/extensions/pi-var/__tests__/unit/cow-git-merge.test.ts +404 -0
  9. package/extensions/pi-var/__tests__/unit/edr.test.ts +119 -0
  10. package/extensions/pi-var/__tests__/unit/environment.test.ts +369 -0
  11. package/extensions/pi-var/__tests__/unit/file-redirect.test.ts +295 -0
  12. package/extensions/pi-var/__tests__/unit/store.test.ts +489 -0
  13. package/extensions/pi-var/__tests__/unit/tools.test.ts +133 -0
  14. package/extensions/pi-var/__tests__/unit/variations.test.ts +352 -0
  15. package/extensions/pi-var/index.ts +111 -0
  16. package/extensions/pi-var/src/config.ts +30 -0
  17. package/extensions/pi-var/src/cow/clone.ts +46 -0
  18. package/extensions/pi-var/src/cow/detection.ts +215 -0
  19. package/extensions/pi-var/src/cow/index.ts +6 -0
  20. package/extensions/pi-var/src/edr/constants.ts +199 -0
  21. package/extensions/pi-var/src/edr/crowdstrike.ts +62 -0
  22. package/extensions/pi-var/src/edr/detection.ts +97 -0
  23. package/extensions/pi-var/src/edr/index.ts +6 -0
  24. package/extensions/pi-var/src/environment/copy-env.ts +79 -0
  25. package/extensions/pi-var/src/environment/detect.ts +91 -0
  26. package/extensions/pi-var/src/environment/index.ts +54 -0
  27. package/extensions/pi-var/src/environment/marker.ts +42 -0
  28. package/extensions/pi-var/src/environment/symlink.ts +82 -0
  29. package/extensions/pi-var/src/gitignore/manager.ts +77 -0
  30. package/extensions/pi-var/src/state/index.ts +7 -0
  31. package/extensions/pi-var/src/state/keys.ts +14 -0
  32. package/extensions/pi-var/src/state/store.ts +110 -0
  33. package/extensions/pi-var/src/state/types.ts +70 -0
  34. package/extensions/pi-var/src/tools/command/clean.ts +87 -0
  35. package/extensions/pi-var/src/tools/command/index.ts +95 -0
  36. package/extensions/pi-var/src/tools/command/status.ts +58 -0
  37. package/extensions/pi-var/src/tools/command/stop.ts +37 -0
  38. package/extensions/pi-var/src/tools/file-redirect.ts +126 -0
  39. package/extensions/pi-var/src/tools/redirect/bash.ts +75 -0
  40. package/extensions/pi-var/src/tools/redirect/edit.ts +60 -0
  41. package/extensions/pi-var/src/tools/redirect/index.ts +64 -0
  42. package/extensions/pi-var/src/tools/redirect/read.ts +61 -0
  43. package/extensions/pi-var/src/tools/redirect/write.ts +60 -0
  44. package/extensions/pi-var/src/tools/variation/create.ts +108 -0
  45. package/extensions/pi-var/src/tools/variation/merge.ts +80 -0
  46. package/extensions/pi-var/src/types/index.ts +139 -0
  47. package/extensions/pi-var/src/utils/names.ts +120 -0
  48. package/extensions/pi-var/src/variation/create-copy.ts +35 -0
  49. package/extensions/pi-var/src/variation/create-cow.ts +81 -0
  50. package/extensions/pi-var/src/variation/create-worktree.ts +31 -0
  51. package/extensions/pi-var/src/variation/create.ts +127 -0
  52. package/extensions/pi-var/src/variation/index.ts +9 -0
  53. package/extensions/pi-var/src/variation/merge/copy.ts +151 -0
  54. package/extensions/pi-var/src/variation/merge/git.ts +176 -0
  55. package/extensions/pi-var/src/variation/merge/index.ts +36 -0
  56. package/extensions/pi-var/src/variation/merge/rsync.ts +74 -0
  57. package/extensions/pi-var/src/variation/merge/strategy.ts +62 -0
  58. package/extensions/pi-var/src/variation/paths.ts +26 -0
  59. package/extensions/pi-var/src/variation/remove.ts +79 -0
  60. package/npm-shrinkwrap.json +6908 -0
  61. package/package.json +63 -0
  62. package/skills/variations/SKILL.md +182 -0
  63. package/tsconfig.json +25 -0
  64. package/vitest.config.ts +19 -0
@@ -0,0 +1,95 @@
1
+ /**
2
+ * /var command registration
3
+ */
4
+
5
+ import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
6
+ import type { VarRuntime } from '../../types/index';
7
+ import type { RuntimeStore } from '../../state/types';
8
+ import { handleStatus, handleList } from './status';
9
+ import { handleCleanStale, handleCleanSpecific } from './clean';
10
+ import { handleStop } from './stop';
11
+
12
+ /** Dependencies for command handlers */
13
+ interface CommandDeps {
14
+ pi: ExtensionAPI;
15
+ getRuntime: (ctx: ExtensionContext) => VarRuntime;
16
+ runtimeStore: RuntimeStore;
17
+ }
18
+
19
+ /**
20
+ * Register the /var command (argument-free, status/overview)
21
+ */
22
+ export function registerVarCommand(pi: ExtensionAPI, deps: CommandDeps): void {
23
+ const { getRuntime, runtimeStore } = deps;
24
+
25
+ pi.registerCommand('var', {
26
+ description: 'Show variation status and provide manual override commands',
27
+ handler: async (args: string, ctx: ExtensionContext) => {
28
+ const trimmed = args.trim();
29
+ const parts = trimmed.split(/\s+/);
30
+ const subcommand = parts[0] || '';
31
+
32
+ const runtime = getRuntime(ctx);
33
+
34
+ // Parse flags
35
+ const staleMatch = trimmed.match(/--stale\s+(\d+)/);
36
+ const staleDays = staleMatch ? parseInt(staleMatch[1], 10) : null;
37
+
38
+ try {
39
+ // No args = status overview
40
+ if (!trimmed) {
41
+ handleStatus(runtime, ctx);
42
+ return;
43
+ }
44
+
45
+ // Manual subcommands (power user features)
46
+ switch (subcommand) {
47
+ case 'list': {
48
+ handleList(runtime, ctx);
49
+ break;
50
+ }
51
+
52
+ case 'clean': {
53
+ // Handle --stale flag
54
+ if (staleDays) {
55
+ await handleCleanStale(runtime, ctx, staleDays, runtimeStore, pi);
56
+ return;
57
+ }
58
+
59
+ // Clean specific variation
60
+ const name = parts[1];
61
+ if (!name) {
62
+ ctx.ui.notify('Usage: /var clean <name> or /var clean --stale <days>', 'warning');
63
+ return;
64
+ }
65
+
66
+ await handleCleanSpecific(runtime, ctx, name, runtimeStore, pi);
67
+ break;
68
+ }
69
+
70
+ case 'stop':
71
+ case 'main': {
72
+ handleStop(runtime, ctx, runtimeStore, pi);
73
+ break;
74
+ }
75
+
76
+ default: {
77
+ ctx.ui.notify(
78
+ `Unknown: ${subcommand}\n\n` +
79
+ 'Usage:\n' +
80
+ ' /var Show status\n' +
81
+ ' /var list List variations\n' +
82
+ ' /var clean <name> Delete variation\n' +
83
+ ' /var clean --stale <d> Delete variations older than d days\n' +
84
+ ' /var stop Return to source (deactivate)',
85
+ 'warning'
86
+ );
87
+ }
88
+ }
89
+ } catch (error) {
90
+ const message = error instanceof Error ? error.message : String(error);
91
+ ctx.ui.notify(`Error: ${message}`, 'error');
92
+ }
93
+ },
94
+ });
95
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * /var command status handlers
3
+ */
4
+
5
+ import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
6
+ import type { VarRuntime } from '../../types/index';
7
+
8
+ export function handleStatus(runtime: VarRuntime, ctx: ExtensionContext): void {
9
+ if (runtime.state.variations.length === 0) {
10
+ ctx.ui.notify(
11
+ 'No active variations.\n\n' +
12
+ 'The AI creates variations automatically via the create_variation tool ' +
13
+ 'when parallel or isolated work is needed.',
14
+ 'info'
15
+ );
16
+ return;
17
+ }
18
+
19
+ const lines = runtime.state.variations.map((v) => {
20
+ const isActive = v.id === runtime.state.activeVariationId;
21
+ const indicator = isActive ? '🌿 ' : ' ';
22
+ const typeIcon = v.type === 'cow' ? '⚡' : v.type === 'worktree' ? 'Git' : 'Copy';
23
+ const branchInfo = v.branchName ? ` → ${v.branchName}` : '';
24
+ return `${indicator}${v.name} [${typeIcon}]${branchInfo}`;
25
+ });
26
+
27
+ const active = runtime.state.activeVariationId
28
+ ? runtime.state.variations.find((v) => v.id === runtime.state.activeVariationId)
29
+ : null;
30
+
31
+ const branchDetail = active?.branchName
32
+ ? `\nBranch: ${active.branchName}` +
33
+ (active.mergeBaseCommit ? ` (base: ${active.mergeBaseCommit.slice(0, 8)})` : '')
34
+ : '';
35
+
36
+ const header = active
37
+ ? `Active variation: "${active.name}"\nAll operations redirect to: ${active.path}${branchDetail}\n`
38
+ : 'No active variation (working in source)';
39
+
40
+ ctx.ui.notify(`${header}\n\nVariations:\n${lines.join('\n')}`, 'info');
41
+ }
42
+
43
+ export function handleList(runtime: VarRuntime, ctx: ExtensionContext): void {
44
+ if (runtime.state.variations.length === 0) {
45
+ ctx.ui.notify('No variations yet. The AI creates them via create_variation tool.', 'info');
46
+ return;
47
+ }
48
+
49
+ const lines = runtime.state.variations.map((v) => {
50
+ const isActive = v.id === runtime.state.activeVariationId;
51
+ const indicator = isActive ? '🌿 ' : ' ';
52
+ const typeIcon = v.type === 'cow' ? '⚡' : v.type === 'worktree' ? 'Git' : 'Copy';
53
+ const branchInfo = v.branchName ? ` → ${v.branchName}` : '';
54
+ return `${indicator}${v.name} [${typeIcon}]${branchInfo}`;
55
+ });
56
+
57
+ ctx.ui.notify(`Variations:\n${lines.join('\n')}`, 'info');
58
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * /var command stop/main handler
3
+ */
4
+
5
+ import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
6
+ import type { VarRuntime } from '../../types/index';
7
+ import type { RuntimeStore } from '../../state/types';
8
+
9
+ function getSessionKey(ctx: ExtensionContext): string {
10
+ return ctx.sessionManager.getSessionId?.() || 'default';
11
+ }
12
+
13
+ export function handleStop(
14
+ runtime: VarRuntime,
15
+ ctx: ExtensionContext,
16
+ runtimeStore: RuntimeStore,
17
+ pi: ExtensionAPI
18
+ ): void {
19
+ // Deactivate current variation (return to source)
20
+ if (runtime.state.activeVariationId) {
21
+ const variation = runtime.state.variations.find(
22
+ (v) => v.id === runtime.state.activeVariationId
23
+ );
24
+ runtime.state.activeVariationId = null;
25
+ ctx.ui.setStatus('pi-var', '');
26
+
27
+ // Persist the "stop" state to session
28
+ runtimeStore.persistState(getSessionKey(ctx), pi);
29
+
30
+ ctx.ui.notify(
31
+ variation ? `Left variation "${variation.name}" — now in source` : 'Now in source directory',
32
+ 'info'
33
+ );
34
+ } else {
35
+ ctx.ui.notify('Already in source directory', 'info');
36
+ }
37
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * File Redirection Tools for pi-var
3
+ *
4
+ * Provides redirected file operations that automatically route read, edit, and write
5
+ * calls to the active variation directory when a variation is active.
6
+ *
7
+ * Path resolution logic:
8
+ * - If no active variation: return inputPath unchanged
9
+ * - If absolute path in source: redirect to variation
10
+ * - If absolute path already in variation: keep as-is
11
+ * - If relative path: resolve against variation path
12
+ */
13
+
14
+ import path from 'node:path';
15
+ import { promises as fs } from 'node:fs';
16
+ import type { VarRuntime, VarState, Variation } from '../types/index';
17
+
18
+ /**
19
+ * Resolve a path to the variation directory when redirection is active
20
+ *
21
+ * @param inputPath - The original path (relative or absolute)
22
+ * @param cwd - Current working directory
23
+ * @param state - The current VarState containing active variation info
24
+ * @returns The resolved path (redirected to variation if active)
25
+ */
26
+ export function resolveVariationPath(inputPath: string, cwd: string, state: VarState): string {
27
+ // No active variation - return path unchanged
28
+ if (!state.activeVariationId) {
29
+ return inputPath;
30
+ }
31
+
32
+ const variation = state.variations.find((v) => v.id === state.activeVariationId);
33
+ if (!variation) {
34
+ return inputPath;
35
+ }
36
+
37
+ // Handle absolute paths
38
+ if (path.isAbsolute(inputPath)) {
39
+ // Check if already in variation directory
40
+ const relativeToVariation = path.relative(variation.path, inputPath);
41
+ if (!relativeToVariation.startsWith('..') && !path.isAbsolute(relativeToVariation)) {
42
+ // Already in variation path - keep as-is
43
+ return inputPath;
44
+ }
45
+
46
+ // Check if in source directory
47
+ const relativeToSource = path.relative(variation.sourcePath, inputPath);
48
+ if (!relativeToSource.startsWith('..') && !path.isAbsolute(relativeToSource)) {
49
+ // In source - redirect to variation
50
+ return path.join(variation.path, relativeToSource);
51
+ }
52
+
53
+ // External file (outside both source and variation) - keep as-is
54
+ return inputPath;
55
+ }
56
+
57
+ // Relative path - resolve against variation path
58
+ return path.join(variation.path, inputPath);
59
+ }
60
+
61
+ /**
62
+ * Create read operations that redirect to the variation directory when active
63
+ */
64
+ export function createRedirectedReadOps(cwd: string, runtime: VarRuntime) {
65
+ return {
66
+ readFile: async (filePath: string): Promise<Buffer> => {
67
+ const resolvedPath = resolveVariationPath(filePath, cwd, runtime.state);
68
+ return fs.readFile(resolvedPath);
69
+ },
70
+
71
+ access: async (filePath: string, mode?: number): Promise<void> => {
72
+ const resolvedPath = resolveVariationPath(filePath, cwd, runtime.state);
73
+ return fs.access(resolvedPath, mode);
74
+ },
75
+
76
+ detectImageMimeType: async (filePath: string): Promise<string | null | undefined> => {
77
+ const resolvedPath = resolveVariationPath(filePath, cwd, runtime.state);
78
+ try {
79
+ const ext = path.extname(resolvedPath).toLowerCase();
80
+ const mimeTypes: Record<string, string> = {
81
+ '.jpg': 'image/jpeg',
82
+ '.jpeg': 'image/jpeg',
83
+ '.png': 'image/png',
84
+ '.gif': 'image/gif',
85
+ '.webp': 'image/webp',
86
+ };
87
+ return mimeTypes[ext] || null;
88
+ } catch {
89
+ return null;
90
+ }
91
+ },
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Create write operations that redirect to the variation directory when active
97
+ */
98
+ export function createRedirectedWriteOps(cwd: string, runtime: VarRuntime) {
99
+ return {
100
+ writeFile: async (filePath: string, content: string | Buffer): Promise<void> => {
101
+ const resolvedPath = resolveVariationPath(filePath, cwd, runtime.state);
102
+ // Ensure parent directory exists
103
+ await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
104
+ await fs.writeFile(resolvedPath, content);
105
+ },
106
+
107
+ mkdir: async (dirPath: string, options?: { recursive?: boolean }): Promise<void> => {
108
+ const resolvedPath = resolveVariationPath(dirPath, cwd, runtime.state);
109
+ await fs.mkdir(resolvedPath, options);
110
+ },
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Create edit operations that redirect to the variation directory when active
116
+ */
117
+ export function createRedirectedEditOps(cwd: string, runtime: VarRuntime) {
118
+ const readOps = createRedirectedReadOps(cwd, runtime);
119
+ const writeOps = createRedirectedWriteOps(cwd, runtime);
120
+
121
+ return {
122
+ readFile: readOps.readFile,
123
+ access: readOps.access,
124
+ writeFile: writeOps.writeFile,
125
+ };
126
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Bash tool override for variation redirection
3
+ */
4
+
5
+ import type {
6
+ ExtensionAPI,
7
+ ExtensionContext,
8
+ AgentToolUpdateCallback,
9
+ } from '@earendil-works/pi-coding-agent';
10
+ import {
11
+ createBashTool,
12
+ type BashSpawnContext,
13
+ type BashToolInput,
14
+ } from '@earendil-works/pi-coding-agent';
15
+ import type { VarRuntime } from '../../types/index';
16
+
17
+ export function createBashHandler(
18
+ getRuntime: (ctx: ExtensionContext) => VarRuntime,
19
+ getDefaultTools: (cwd: string) => {
20
+ bash: ReturnType<typeof createBashTool>;
21
+ }
22
+ ) {
23
+ return {
24
+ name: 'bash' as const,
25
+ label: 'bash',
26
+ description:
27
+ 'Execute a bash command. Returns stdout and stderr. ' +
28
+ 'Output is truncated to last 2000 lines or 50KB (whichever is hit first). ' +
29
+ 'When in a variation, executes in the variation directory.',
30
+ parameters: createBashTool('').parameters,
31
+
32
+ async execute(
33
+ toolCallId: string,
34
+ params: BashToolInput,
35
+ signal: AbortSignal,
36
+ onUpdate: AgentToolUpdateCallback<unknown> | undefined,
37
+ ctx: ExtensionContext
38
+ ) {
39
+ const runtime = getRuntime(ctx);
40
+
41
+ // No active variation - use default behavior with ctx.cwd
42
+ if (!runtime.state.activeVariationId) {
43
+ const tools = getDefaultTools(ctx.cwd);
44
+ return tools.bash.execute(toolCallId, params, signal, onUpdate);
45
+ }
46
+
47
+ // Get active variation
48
+ const variation = runtime.state.variations.find(
49
+ (v) => v.id === runtime.state.activeVariationId
50
+ );
51
+
52
+ if (!variation) {
53
+ const tools = getDefaultTools(ctx.cwd);
54
+ return tools.bash.execute(toolCallId, params, signal, onUpdate);
55
+ }
56
+
57
+ // Create bash tool with variation as cwd and spawn hook for env vars
58
+ const tool = createBashTool(variation.path, {
59
+ spawnHook: (context: BashSpawnContext): BashSpawnContext => ({
60
+ ...context,
61
+ cwd: variation.path,
62
+ env: {
63
+ ...context.env,
64
+ PI_VARIATION: variation.name,
65
+ PI_VARIATION_PATH: variation.path,
66
+ PI_SOURCE_PATH: variation.sourcePath,
67
+ PI_VARIATION_TYPE: variation.type,
68
+ },
69
+ }),
70
+ });
71
+
72
+ return tool.execute(toolCallId, params, signal, onUpdate);
73
+ },
74
+ };
75
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Edit tool override for variation redirection
3
+ */
4
+
5
+ import type {
6
+ ExtensionAPI,
7
+ ExtensionContext,
8
+ AgentToolUpdateCallback,
9
+ } from '@earendil-works/pi-coding-agent';
10
+ import { createEditTool, type EditToolInput } from '@earendil-works/pi-coding-agent';
11
+ import type { VarRuntime } from '../../types/index';
12
+ import { createRedirectedEditOps } from '../file-redirect';
13
+
14
+ export function createEditHandler(
15
+ getRuntime: (ctx: ExtensionContext) => VarRuntime,
16
+ getDefaultTools: (cwd: string) => {
17
+ edit: ReturnType<typeof createEditTool>;
18
+ }
19
+ ) {
20
+ return {
21
+ name: 'edit' as const,
22
+ label: 'edit',
23
+ description:
24
+ 'Edit a file by replacing exact text. The oldText must match exactly ' +
25
+ '(including whitespace). Use this for precise, surgical edits. ' +
26
+ 'When in a variation, edits in the variation directory.',
27
+ parameters: createEditTool('').parameters,
28
+
29
+ async execute(
30
+ toolCallId: string,
31
+ params: EditToolInput,
32
+ signal: AbortSignal,
33
+ onUpdate: AgentToolUpdateCallback<unknown> | undefined,
34
+ ctx: ExtensionContext
35
+ ) {
36
+ const runtime = getRuntime(ctx);
37
+
38
+ // No active variation - use default behavior
39
+ if (!runtime.state.activeVariationId) {
40
+ const tools = getDefaultTools(ctx.cwd);
41
+ return tools.edit.execute(toolCallId, params, signal, onUpdate);
42
+ }
43
+
44
+ // Get active variation
45
+ const variation = runtime.state.variations.find(
46
+ (v) => v.id === runtime.state.activeVariationId
47
+ );
48
+
49
+ if (!variation) {
50
+ const tools = getDefaultTools(ctx.cwd);
51
+ return tools.edit.execute(toolCallId, params, signal, onUpdate);
52
+ }
53
+
54
+ // Create tool with redirected operations
55
+ const ops = createRedirectedEditOps(ctx.cwd, runtime);
56
+ const tool = createEditTool(ctx.cwd, { operations: ops });
57
+ return tool.execute(toolCallId, params, signal, onUpdate);
58
+ },
59
+ };
60
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Redirected tools registration module
3
+ */
4
+
5
+ import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
6
+ import {
7
+ createReadTool,
8
+ createEditTool,
9
+ createWriteTool,
10
+ createBashTool,
11
+ } from '@earendil-works/pi-coding-agent';
12
+ import type { VarRuntime } from '../../types/index';
13
+ import { createReadHandler } from './read';
14
+ import { createEditHandler } from './edit';
15
+ import { createWriteHandler } from './write';
16
+ import { createBashHandler } from './bash';
17
+
18
+ /**
19
+ * Get runtime function type
20
+ */
21
+ type GetRuntime = (ctx: ExtensionContext) => VarRuntime;
22
+
23
+ /**
24
+ * Register redirected file tools (read, edit, write) and bash tool.
25
+ * These override the built-in tools to redirect operations to the active variation
26
+ * when a variation is active.
27
+ */
28
+ export function registerRedirectedTools(pi: ExtensionAPI, getRuntime: GetRuntime): void {
29
+ // Cache for default tools by cwd to avoid recreating
30
+ const defaultTools = new Map<
31
+ string,
32
+ {
33
+ read: ReturnType<typeof createReadTool>;
34
+ edit: ReturnType<typeof createEditTool>;
35
+ write: ReturnType<typeof createWriteTool>;
36
+ bash: ReturnType<typeof createBashTool>;
37
+ }
38
+ >();
39
+
40
+ function getDefaultTools(cwd: string) {
41
+ let tools = defaultTools.get(cwd);
42
+ if (!tools) {
43
+ tools = {
44
+ read: createReadTool(cwd),
45
+ edit: createEditTool(cwd),
46
+ write: createWriteTool(cwd),
47
+ bash: createBashTool(cwd),
48
+ };
49
+ defaultTools.set(cwd, tools);
50
+ }
51
+ return tools;
52
+ }
53
+
54
+ // Register all redirected tools
55
+ const readTool = createReadHandler(getRuntime, getDefaultTools);
56
+ const editTool = createEditHandler(getRuntime, getDefaultTools);
57
+ const writeTool = createWriteHandler(getRuntime, getDefaultTools);
58
+ const bashTool = createBashHandler(getRuntime, getDefaultTools);
59
+
60
+ pi.registerTool(readTool);
61
+ pi.registerTool(editTool);
62
+ pi.registerTool(writeTool);
63
+ pi.registerTool(bashTool);
64
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Read tool override for variation redirection
3
+ */
4
+
5
+ import type {
6
+ ExtensionAPI,
7
+ ExtensionContext,
8
+ AgentToolUpdateCallback,
9
+ } from '@earendil-works/pi-coding-agent';
10
+ import { createReadTool, type ReadToolInput } from '@earendil-works/pi-coding-agent';
11
+ import type { VarRuntime } from '../../types/index';
12
+ import { createRedirectedReadOps } from '../file-redirect';
13
+
14
+ export function createReadHandler(
15
+ getRuntime: (ctx: ExtensionContext) => VarRuntime,
16
+ getDefaultTools: (cwd: string) => {
17
+ read: ReturnType<typeof createReadTool>;
18
+ }
19
+ ) {
20
+ return {
21
+ name: 'read' as const,
22
+ label: 'read',
23
+ description:
24
+ 'Read the contents of a file. Supports text files and images (jpg, png, gif, webp). ' +
25
+ 'Images are sent as attachments. For text files, output is truncated to 2000 lines ' +
26
+ 'or 50KB (whichever is hit first). Use offset/limit for large files. ' +
27
+ 'When in a variation, reads from the variation directory.',
28
+ parameters: createReadTool('').parameters,
29
+
30
+ async execute(
31
+ toolCallId: string,
32
+ params: ReadToolInput,
33
+ signal: AbortSignal,
34
+ onUpdate: AgentToolUpdateCallback<unknown> | undefined,
35
+ ctx: ExtensionContext
36
+ ) {
37
+ const runtime = getRuntime(ctx);
38
+
39
+ // No active variation - use default behavior
40
+ if (!runtime.state.activeVariationId) {
41
+ const tools = getDefaultTools(ctx.cwd);
42
+ return tools.read.execute(toolCallId, params, signal, onUpdate);
43
+ }
44
+
45
+ // Get active variation
46
+ const variation = runtime.state.variations.find(
47
+ (v) => v.id === runtime.state.activeVariationId
48
+ );
49
+
50
+ if (!variation) {
51
+ const tools = getDefaultTools(ctx.cwd);
52
+ return tools.read.execute(toolCallId, params, signal, onUpdate);
53
+ }
54
+
55
+ // Create tool with redirected operations
56
+ const ops = createRedirectedReadOps(ctx.cwd, runtime);
57
+ const tool = createReadTool(ctx.cwd, { operations: ops });
58
+ return tool.execute(toolCallId, params, signal, onUpdate);
59
+ },
60
+ };
61
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Write tool override for variation redirection
3
+ */
4
+
5
+ import type {
6
+ ExtensionAPI,
7
+ ExtensionContext,
8
+ AgentToolUpdateCallback,
9
+ } from '@earendil-works/pi-coding-agent';
10
+ import { createWriteTool, type WriteToolInput } from '@earendil-works/pi-coding-agent';
11
+ import type { VarRuntime } from '../../types/index';
12
+ import { createRedirectedWriteOps } from '../file-redirect';
13
+
14
+ export function createWriteHandler(
15
+ getRuntime: (ctx: ExtensionContext) => VarRuntime,
16
+ getDefaultTools: (cwd: string) => {
17
+ write: ReturnType<typeof createWriteTool>;
18
+ }
19
+ ) {
20
+ return {
21
+ name: 'write' as const,
22
+ label: 'write',
23
+ description:
24
+ 'Write content to a file. Creates the file if it does not exist, overwrites if it does. ' +
25
+ 'Automatically creates parent directories. ' +
26
+ 'When in a variation, writes to the variation directory.',
27
+ parameters: createWriteTool('').parameters,
28
+
29
+ async execute(
30
+ toolCallId: string,
31
+ params: WriteToolInput,
32
+ signal: AbortSignal,
33
+ onUpdate: AgentToolUpdateCallback<unknown> | undefined,
34
+ ctx: ExtensionContext
35
+ ) {
36
+ const runtime = getRuntime(ctx);
37
+
38
+ // No active variation - use default behavior
39
+ if (!runtime.state.activeVariationId) {
40
+ const tools = getDefaultTools(ctx.cwd);
41
+ return tools.write.execute(toolCallId, params, signal, onUpdate);
42
+ }
43
+
44
+ // Get active variation
45
+ const variation = runtime.state.variations.find(
46
+ (v) => v.id === runtime.state.activeVariationId
47
+ );
48
+
49
+ if (!variation) {
50
+ const tools = getDefaultTools(ctx.cwd);
51
+ return tools.write.execute(toolCallId, params, signal, onUpdate);
52
+ }
53
+
54
+ // Create tool with redirected operations
55
+ const ops = createRedirectedWriteOps(ctx.cwd, runtime);
56
+ const tool = createWriteTool(ctx.cwd, { operations: ops });
57
+ return tool.execute(toolCallId, params, signal, onUpdate);
58
+ },
59
+ };
60
+ }