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.
- package/.github/FUNDING.yml +4 -0
- package/.prettierrc +7 -0
- package/README.md +240 -0
- package/commitlint.config.cjs +3 -0
- package/extensions/pi-var/__tests__/integration/smoke.test.ts +308 -0
- package/extensions/pi-var/__tests__/unit/command.test.ts +322 -0
- package/extensions/pi-var/__tests__/unit/cow-detection.test.ts +340 -0
- package/extensions/pi-var/__tests__/unit/cow-git-merge.test.ts +404 -0
- package/extensions/pi-var/__tests__/unit/edr.test.ts +119 -0
- package/extensions/pi-var/__tests__/unit/environment.test.ts +369 -0
- package/extensions/pi-var/__tests__/unit/file-redirect.test.ts +295 -0
- package/extensions/pi-var/__tests__/unit/store.test.ts +489 -0
- package/extensions/pi-var/__tests__/unit/tools.test.ts +133 -0
- package/extensions/pi-var/__tests__/unit/variations.test.ts +352 -0
- package/extensions/pi-var/index.ts +111 -0
- package/extensions/pi-var/src/config.ts +30 -0
- package/extensions/pi-var/src/cow/clone.ts +46 -0
- package/extensions/pi-var/src/cow/detection.ts +215 -0
- package/extensions/pi-var/src/cow/index.ts +6 -0
- package/extensions/pi-var/src/edr/constants.ts +199 -0
- package/extensions/pi-var/src/edr/crowdstrike.ts +62 -0
- package/extensions/pi-var/src/edr/detection.ts +97 -0
- package/extensions/pi-var/src/edr/index.ts +6 -0
- package/extensions/pi-var/src/environment/copy-env.ts +79 -0
- package/extensions/pi-var/src/environment/detect.ts +91 -0
- package/extensions/pi-var/src/environment/index.ts +54 -0
- package/extensions/pi-var/src/environment/marker.ts +42 -0
- package/extensions/pi-var/src/environment/symlink.ts +82 -0
- package/extensions/pi-var/src/gitignore/manager.ts +77 -0
- package/extensions/pi-var/src/state/index.ts +7 -0
- package/extensions/pi-var/src/state/keys.ts +14 -0
- package/extensions/pi-var/src/state/store.ts +110 -0
- package/extensions/pi-var/src/state/types.ts +70 -0
- package/extensions/pi-var/src/tools/command/clean.ts +87 -0
- package/extensions/pi-var/src/tools/command/index.ts +95 -0
- package/extensions/pi-var/src/tools/command/status.ts +58 -0
- package/extensions/pi-var/src/tools/command/stop.ts +37 -0
- package/extensions/pi-var/src/tools/file-redirect.ts +126 -0
- package/extensions/pi-var/src/tools/redirect/bash.ts +75 -0
- package/extensions/pi-var/src/tools/redirect/edit.ts +60 -0
- package/extensions/pi-var/src/tools/redirect/index.ts +64 -0
- package/extensions/pi-var/src/tools/redirect/read.ts +61 -0
- package/extensions/pi-var/src/tools/redirect/write.ts +60 -0
- package/extensions/pi-var/src/tools/variation/create.ts +108 -0
- package/extensions/pi-var/src/tools/variation/merge.ts +80 -0
- package/extensions/pi-var/src/types/index.ts +139 -0
- package/extensions/pi-var/src/utils/names.ts +120 -0
- package/extensions/pi-var/src/variation/create-copy.ts +35 -0
- package/extensions/pi-var/src/variation/create-cow.ts +81 -0
- package/extensions/pi-var/src/variation/create-worktree.ts +31 -0
- package/extensions/pi-var/src/variation/create.ts +127 -0
- package/extensions/pi-var/src/variation/index.ts +9 -0
- package/extensions/pi-var/src/variation/merge/copy.ts +151 -0
- package/extensions/pi-var/src/variation/merge/git.ts +176 -0
- package/extensions/pi-var/src/variation/merge/index.ts +36 -0
- package/extensions/pi-var/src/variation/merge/rsync.ts +74 -0
- package/extensions/pi-var/src/variation/merge/strategy.ts +62 -0
- package/extensions/pi-var/src/variation/paths.ts +26 -0
- package/extensions/pi-var/src/variation/remove.ts +79 -0
- package/npm-shrinkwrap.json +6908 -0
- package/package.json +63 -0
- package/skills/variations/SKILL.md +182 -0
- package/tsconfig.json +25 -0
- 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
|
+
}
|