pum-agent 0.1.0-beta.3
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/LICENSE +21 -0
- package/README.md +196 -0
- package/package.json +69 -0
- package/src/agent-selector.tsx +217 -0
- package/src/agent-usage.ts +93 -0
- package/src/animation.tsx +476 -0
- package/src/app.tsx +1953 -0
- package/src/apply-patch.ts +583 -0
- package/src/cancel-confirmation.ts +14 -0
- package/src/check-mode.ts +630 -0
- package/src/commands.ts +45 -0
- package/src/config.ts +24 -0
- package/src/explanation-strength.ts +47 -0
- package/src/git-branch.ts +54 -0
- package/src/help-popup.tsx +279 -0
- package/src/history.ts +57 -0
- package/src/image-paste.ts +204 -0
- package/src/index.tsx +133 -0
- package/src/login-controller.ts +267 -0
- package/src/login-flow.ts +170 -0
- package/src/login-popup.tsx +154 -0
- package/src/platform.ts +94 -0
- package/src/prompt-stash.ts +130 -0
- package/src/replay.ts +188 -0
- package/src/session-history-popup.tsx +68 -0
- package/src/settings-popup.tsx +283 -0
- package/src/settings.ts +81 -0
- package/src/shutdown.ts +23 -0
- package/src/stash-batch.ts +28 -0
- package/src/status-bar.tsx +143 -0
- package/src/status-metadata.ts +110 -0
- package/src/subagents/manager.ts +1196 -0
- package/src/subagents/types.ts +86 -0
- package/src/syntax.ts +60 -0
- package/src/theme.ts +346 -0
- package/src/tool-line.ts +72 -0
- package/src/transcript.tsx +393 -0
- package/src/web-search.ts +157 -0
- package/src/worktree-command.ts +39 -0
- package/src/worktree.ts +219 -0
- package/src/writing-style.ts +54 -0
package/src/worktree.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, realpath, writeFile } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, posix, resolve, win32 } from "node:path";
|
|
4
|
+
import { isPathInside, type RuntimePlatform } from "./platform";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
const ADJECTIVES = [
|
|
11
|
+
"amber", "brisk", "calm", "cedar", "cobalt", "coral", "crisp", "dawn",
|
|
12
|
+
"ember", "frost", "golden", "jade", "lunar", "misty", "quiet", "silver",
|
|
13
|
+
];
|
|
14
|
+
const NOUNS = [
|
|
15
|
+
"badger", "falcon", "fox", "heron", "lynx", "marten", "otter", "owl",
|
|
16
|
+
"panda", "raven", "seal", "sparrow", "tiger", "wolf", "wren", "yak",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export type WorktreeRecord = {
|
|
20
|
+
name: string;
|
|
21
|
+
path: string;
|
|
22
|
+
branch: string;
|
|
23
|
+
baseBranch: string;
|
|
24
|
+
baseCommit: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
async function git(cwd: string, args: string[]): Promise<string> {
|
|
28
|
+
const result = await execFileAsync("git", args, {
|
|
29
|
+
cwd,
|
|
30
|
+
encoding: "utf8",
|
|
31
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
32
|
+
});
|
|
33
|
+
return result.stdout.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function safeName(value: string): string {
|
|
37
|
+
const normalized = value
|
|
38
|
+
.trim()
|
|
39
|
+
.toLowerCase()
|
|
40
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
41
|
+
.replace(/^-+|-+$/g, "")
|
|
42
|
+
.replace(/-+/g, "-");
|
|
43
|
+
if (!normalized) throw new Error("Worktree name must contain a letter or number");
|
|
44
|
+
return normalized.slice(0, 48);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function randomWorktreeName(): string {
|
|
48
|
+
const bytes = randomBytes(4);
|
|
49
|
+
const adjective = ADJECTIVES[bytes[0]! % ADJECTIVES.length]!;
|
|
50
|
+
const noun = NOUNS[bytes[1]! % NOUNS.length]!;
|
|
51
|
+
return `${adjective}-${noun}-${bytes.toString("hex").slice(0, 4)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function repositoryRoot(cwd: string): Promise<string> {
|
|
55
|
+
return realpath(await git(cwd, ["rev-parse", "--show-toplevel"]));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function managedRoot(root: string): Promise<string> {
|
|
59
|
+
const directory = await realpath(resolve(root, ".pum", "worktrees"));
|
|
60
|
+
if (!isPathInside(root, directory)) {
|
|
61
|
+
throw new Error(`Managed worktree directory resolves outside the project: ${directory}`);
|
|
62
|
+
}
|
|
63
|
+
return directory;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function managedWorktreePath(cwd: string, path: string): Promise<string> {
|
|
67
|
+
const root = await repositoryRoot(cwd);
|
|
68
|
+
const parent = await managedRoot(root);
|
|
69
|
+
const canonical = await realpath(path);
|
|
70
|
+
if (!isPathInside(parent, canonical)) {
|
|
71
|
+
throw new Error(`Worktree path resolves outside the managed directory: ${path}`);
|
|
72
|
+
}
|
|
73
|
+
return canonical;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function excludeManagedDirectory(root: string): Promise<void> {
|
|
77
|
+
const rawExcludePath = await git(root, ["rev-parse", "--git-path", "info/exclude"]);
|
|
78
|
+
const excludePath = isAbsolute(rawExcludePath) ? rawExcludePath : resolve(root, rawExcludePath);
|
|
79
|
+
let current = "";
|
|
80
|
+
try {
|
|
81
|
+
current = await readFile(excludePath, "utf8");
|
|
82
|
+
} catch {
|
|
83
|
+
// Git creates this file lazily.
|
|
84
|
+
}
|
|
85
|
+
const rule = ".pum/";
|
|
86
|
+
if (current.split(/\r?\n/).includes(rule)) return;
|
|
87
|
+
const prefix = current && !current.endsWith("\n") ? "\n" : "";
|
|
88
|
+
await writeFile(excludePath, `${current}${prefix}${rule}\n`, "utf8");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function createWorktree(cwd: string, requestedName?: string): Promise<WorktreeRecord> {
|
|
92
|
+
const root = await repositoryRoot(cwd);
|
|
93
|
+
const baseBranch = await git(root, ["branch", "--show-current"]);
|
|
94
|
+
if (!baseBranch) throw new Error("Cannot create a PUM worktree from a detached HEAD");
|
|
95
|
+
const baseCommit = await git(root, ["rev-parse", "HEAD"]);
|
|
96
|
+
const name = safeName(requestedName || randomWorktreeName());
|
|
97
|
+
const branch = `pum/${name}`;
|
|
98
|
+
const directory = resolve(root, ".pum", "worktrees", name);
|
|
99
|
+
|
|
100
|
+
await mkdir(join(root, ".pum", "worktrees"), { recursive: true });
|
|
101
|
+
const parent = await managedRoot(root);
|
|
102
|
+
await excludeManagedDirectory(root);
|
|
103
|
+
await git(root, ["worktree", "add", "-b", branch, directory, baseCommit]);
|
|
104
|
+
|
|
105
|
+
const canonicalDirectory = await realpath(directory);
|
|
106
|
+
if (!isPathInside(parent, canonicalDirectory)) {
|
|
107
|
+
throw new Error(`Created worktree resolves outside the managed directory: ${canonicalDirectory}`);
|
|
108
|
+
}
|
|
109
|
+
return { name, path: canonicalDirectory, branch, baseBranch, baseCommit };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseWorktreeRecords(
|
|
113
|
+
output: string,
|
|
114
|
+
platform: RuntimePlatform,
|
|
115
|
+
): WorktreeRecord[] {
|
|
116
|
+
const nulDelimited = output.includes("\0");
|
|
117
|
+
const blocks = nulDelimited ? output.split(/\0\0+/) : output.split(/\r?\n\r?\n+/);
|
|
118
|
+
const paths = platform === "win32" ? win32 : posix;
|
|
119
|
+
const records: WorktreeRecord[] = [];
|
|
120
|
+
|
|
121
|
+
for (const block of blocks) {
|
|
122
|
+
const fields = new Map<string, string>();
|
|
123
|
+
const lines = nulDelimited ? block.split("\0") : block.split(/\r?\n/);
|
|
124
|
+
for (const rawLine of lines) {
|
|
125
|
+
const line = rawLine.replace(/\r$/, "");
|
|
126
|
+
const space = line.indexOf(" ");
|
|
127
|
+
if (space > 0) fields.set(line.slice(0, space), line.slice(space + 1));
|
|
128
|
+
}
|
|
129
|
+
const path = fields.get("worktree");
|
|
130
|
+
if (!path) continue;
|
|
131
|
+
const branchRef = fields.get("branch") ?? "";
|
|
132
|
+
records.push({
|
|
133
|
+
name: paths.basename(path),
|
|
134
|
+
path,
|
|
135
|
+
branch: branchRef.replace(/^refs\/heads\//, ""),
|
|
136
|
+
baseBranch: "",
|
|
137
|
+
baseCommit: fields.get("HEAD") ?? "",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return records;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function parseWorktreePorcelain(
|
|
144
|
+
output: string,
|
|
145
|
+
managedRoot: string,
|
|
146
|
+
platform: RuntimePlatform = process.platform,
|
|
147
|
+
): WorktreeRecord[] {
|
|
148
|
+
return parseWorktreeRecords(output, platform)
|
|
149
|
+
.filter((record) => isPathInside(managedRoot, record.path, platform));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function canonicalizeManagedWorktreeRecords(
|
|
153
|
+
records: WorktreeRecord[],
|
|
154
|
+
parent: string,
|
|
155
|
+
platform: RuntimePlatform = process.platform,
|
|
156
|
+
resolvePath: (path: string) => Promise<string> = realpath,
|
|
157
|
+
): Promise<WorktreeRecord[]> {
|
|
158
|
+
const paths = platform === "win32" ? win32 : posix;
|
|
159
|
+
const canonicalParent = await resolvePath(parent);
|
|
160
|
+
const canonicalRecords: WorktreeRecord[] = [];
|
|
161
|
+
for (const record of records) {
|
|
162
|
+
let path: string;
|
|
163
|
+
try {
|
|
164
|
+
path = await resolvePath(record.path);
|
|
165
|
+
} catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!isPathInside(canonicalParent, path, platform)) continue;
|
|
169
|
+
canonicalRecords.push({ ...record, name: paths.basename(path), path });
|
|
170
|
+
}
|
|
171
|
+
return canonicalRecords;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function listWorktrees(cwd: string): Promise<WorktreeRecord[]> {
|
|
175
|
+
const root = await repositoryRoot(cwd);
|
|
176
|
+
let parent: string;
|
|
177
|
+
try {
|
|
178
|
+
parent = await managedRoot(root);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
const output = await git(root, ["worktree", "list", "--porcelain", "-z"]);
|
|
184
|
+
const records = parseWorktreeRecords(output, process.platform);
|
|
185
|
+
return canonicalizeManagedWorktreeRecords(records, parent);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function worktreeStatus(cwd: string, record: WorktreeRecord): Promise<string> {
|
|
189
|
+
const path = await managedWorktreePath(cwd, record.path);
|
|
190
|
+
const status = await git(path, ["status", "--short", "--branch"]);
|
|
191
|
+
return status || `## ${record.branch}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function mergeWorktree(cwd: string, record: WorktreeRecord): Promise<string> {
|
|
195
|
+
const root = await repositoryRoot(cwd);
|
|
196
|
+
const path = await managedWorktreePath(root, record.path);
|
|
197
|
+
const mainStatus = await git(root, ["status", "--porcelain"]);
|
|
198
|
+
if (mainStatus) throw new Error(`The current worktree must be clean before merging:\n${mainStatus}`);
|
|
199
|
+
const childStatus = await git(path, ["status", "--porcelain"]);
|
|
200
|
+
if (childStatus) throw new Error(`Worktree ${record.name} has uncommitted changes`);
|
|
201
|
+
return git(root, ["merge", "--no-ff", record.branch]);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function removeWorktree(
|
|
205
|
+
cwd: string,
|
|
206
|
+
record: WorktreeRecord,
|
|
207
|
+
force = false,
|
|
208
|
+
): Promise<void> {
|
|
209
|
+
const root = await repositoryRoot(cwd);
|
|
210
|
+
const path = await managedWorktreePath(root, record.path);
|
|
211
|
+
if (!force) {
|
|
212
|
+
const merged = await git(root, ["branch", "--merged", "HEAD", "--format=%(refname:short)"]);
|
|
213
|
+
if (!merged.split(/\r?\n/).includes(record.branch)) {
|
|
214
|
+
throw new Error(`Branch ${record.branch} is not merged; use force to remove it`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
await git(root, ["worktree", "remove", ...(force ? ["--force"] : []), path]);
|
|
218
|
+
await git(root, ["branch", force ? "-D" : "-d", record.branch]);
|
|
219
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { InlineExtension } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export const WRITING_STYLES = ["none", "STE"] as const;
|
|
4
|
+
export type WritingStyle = (typeof WRITING_STYLES)[number];
|
|
5
|
+
|
|
6
|
+
let currentStyle: WritingStyle = "none";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Practical ASD-STE100 guidance for model output. The controlled dictionary is
|
|
10
|
+
* not embedded, so PUM does not claim that generated text is formally
|
|
11
|
+
* certified or fully compliant with the standard.
|
|
12
|
+
*/
|
|
13
|
+
export const STE_SYSTEM_PROMPT = `## Writing style: Simplified Technical English (STE)
|
|
14
|
+
|
|
15
|
+
Write your explanatory text with the principles of ASD-STE100 Simplified Technical English.
|
|
16
|
+
|
|
17
|
+
- Keep the technical meaning accurate. Accuracy has priority over simplification.
|
|
18
|
+
- Use simple and unambiguous words. Use one word for one meaning when possible.
|
|
19
|
+
- Use necessary project terms, code identifiers, commands, paths, API names, and other technical nouns and verbs unchanged.
|
|
20
|
+
- Use the active voice. For instructions, use the imperative form.
|
|
21
|
+
- Give only one instruction in each sentence.
|
|
22
|
+
- Keep procedural sentences to 20 words or fewer.
|
|
23
|
+
- Keep descriptive sentences to 25 words or fewer.
|
|
24
|
+
- Use short paragraphs. Keep one topic in each paragraph.
|
|
25
|
+
- Use vertical lists for complex information or multiple actions.
|
|
26
|
+
- Do not use contractions. Do not omit necessary articles, subjects, or verbs.
|
|
27
|
+
- Avoid ambiguous pronouns, idioms, slang, phrasal verbs, and unnecessary synonyms.
|
|
28
|
+
- Repeat a noun when a pronoun could have more than one meaning.
|
|
29
|
+
- Keep terminology and wording consistent.
|
|
30
|
+
- Do not modify quoted text, source code, tool output, or user-supplied text to make it follow STE.
|
|
31
|
+
- Do not state or imply that the output has formal ASD approval or certified STE compliance.
|
|
32
|
+
- Do not mention this writing-style instruction unless the user asks about it.`;
|
|
33
|
+
|
|
34
|
+
export function isWritingStyle(value: unknown): value is WritingStyle {
|
|
35
|
+
return WRITING_STYLES.includes(value as WritingStyle);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function setWritingStyle(style: WritingStyle): void {
|
|
39
|
+
currentStyle = style;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getWritingStyle(): WritingStyle {
|
|
43
|
+
return currentStyle;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const writingStyleExtension: InlineExtension = {
|
|
47
|
+
name: "pum-writing-style",
|
|
48
|
+
factory(pi) {
|
|
49
|
+
pi.on("before_agent_start", (event) => {
|
|
50
|
+
if (currentStyle !== "STE") return;
|
|
51
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${STE_SYSTEM_PROMPT}` };
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
};
|