@rol1n/pi-kit 0.1.2 → 0.1.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/README.md +131 -56
- package/README_zh.md +131 -56
- package/ask-u-questions/README.md +2 -2
- package/continue-signal/README.md +2 -2
- package/custom-providers/README.md +2 -2
- package/image-generate/README.md +4 -2
- package/memory-log/README.md +2 -2
- package/model-vision/README.md +2 -2
- package/model-vision/image.ts +84 -0
- package/model-vision/index.ts +124 -0
- package/model-vision/network.ts +82 -0
- package/model-vision/requests.ts +151 -0
- package/multi-harness/README.md +4 -4
- package/oai-subquota/README.md +2 -2
- package/package.json +18 -24
- package/prompt-snipaste/README.md +19 -5
- package/prompt-snipaste/index.ts +4 -4
- package/prompt-snipaste/snippets.ts +28 -149
- package/prompt-snipaste/types.ts +1 -0
- package/screenshot/README.md +2 -2
- package/screenshot/index.ts +126 -0
- package/scripts/check-package.mjs +54 -0
- package/skills/worktree-studio/SKILL.md +99 -0
- package/srp-theme/README.md +2 -2
- package/subagent/README.md +4 -4
- package/tui-asr/README.md +4 -2
- package/web-reach/README.md +2 -2
- package/web-reach/html.ts +83 -0
- package/web-reach/index.ts +82 -0
- package/web-reach/page.ts +214 -0
- package/web-reach/rsc.ts +161 -0
- package/web-reach/search.ts +85 -0
- package/web-reach/transport.ts +35 -0
- package/windows-wheel-fix/README.md +2 -2
- /package/{prompt-snipaste/prompts → prompts}/ask-questions.md +0 -0
- /package/{prompt-snipaste/prompts → prompts}/delegate-exploration.md +0 -0
- /package/{prompt-snipaste/prompts → prompts}/diagnose-report.md +0 -0
- /package/{prompt-snipaste/prompts → prompts}/frontend-tech-preset.md +0 -0
- /package/{prompt-snipaste/prompts → prompts}/orchestrator-mode.md +0 -0
- /package/{prompt-snipaste/prompts → prompts}/session-kickoff.md +0 -0
- /package/{prompt-snipaste/prompts → prompts}/verify-not-assume.md +0 -0
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
6
|
-
const BUNDLED_PROMPTS_DIR = join(dirname(fileURLToPath(import.meta.url)), "prompts");
|
|
7
|
-
import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { basename, resolve } from "node:path";
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
4
|
import type { Snippet } from "./types.ts";
|
|
9
5
|
|
|
10
6
|
export function parseSnippet(filename: string, raw: string): Snippet | null {
|
|
@@ -48,159 +44,42 @@ export function parseSnippet(filename: string, raw: string): Snippet | null {
|
|
|
48
44
|
};
|
|
49
45
|
}
|
|
50
46
|
|
|
51
|
-
|
|
52
|
-
const trimmed = rawPath.trim();
|
|
53
|
-
if (trimmed === "~" || trimmed.startsWith("~/") || (process.platform === "win32" && trimmed.startsWith("~\\"))) {
|
|
54
|
-
return resolve(homedir(), trimmed.replace(/^~[/\\]?/, ""));
|
|
55
|
-
}
|
|
56
|
-
if (isAbsolute(trimmed)) {
|
|
57
|
-
return resolve(trimmed);
|
|
58
|
-
}
|
|
59
|
-
const fromCwd = resolve(cwd, trimmed);
|
|
60
|
-
if (existsSync(fromCwd)) return fromCwd;
|
|
61
|
-
const fromAgent = resolve(getAgentDir(), trimmed);
|
|
62
|
-
if (existsSync(fromAgent)) return fromAgent;
|
|
63
|
-
return fromCwd;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function getSettingsPrompts(cwd: string, pi?: ExtensionAPI): string[] {
|
|
67
|
-
const results: string[] = [];
|
|
68
|
-
|
|
69
|
-
if (pi) {
|
|
70
|
-
try {
|
|
71
|
-
const settings = pi.getSettings();
|
|
72
|
-
if (Array.isArray(settings?.prompts)) {
|
|
73
|
-
results.push(...settings.prompts);
|
|
74
|
-
return results;
|
|
75
|
-
}
|
|
76
|
-
} catch {
|
|
77
|
-
// pi.getSettings() may throw before runner binds
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const settingsFiles = [
|
|
82
|
-
join(getAgentDir(), "settings.json"),
|
|
83
|
-
join(cwd, CONFIG_DIR_NAME, "settings.json"),
|
|
84
|
-
];
|
|
85
|
-
|
|
86
|
-
for (const sf of settingsFiles) {
|
|
87
|
-
if (!existsSync(sf)) continue;
|
|
88
|
-
try {
|
|
89
|
-
const content = readFileSync(sf, "utf-8");
|
|
90
|
-
const parsed = JSON.parse(content.replace(/^\uFEFF/, ""));
|
|
91
|
-
if (Array.isArray(parsed?.prompts)) {
|
|
92
|
-
for (const p of parsed.prompts) {
|
|
93
|
-
if (typeof p === "string" && !results.includes(p)) {
|
|
94
|
-
results.push(p);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
} catch {
|
|
99
|
-
// ignore JSON parse errors
|
|
100
|
-
}
|
|
101
|
-
}
|
|
47
|
+
const UNREADABLE_PROMPT_CODES = new Set(["ENOENT", "ENOTDIR", "EISDIR", "EACCES", "EPERM"]);
|
|
102
48
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Pi owns prompt discovery, trust, and resource filtering. Consume only its
|
|
51
|
+
* enabled prompt commands; never scan directories or revive excluded templates.
|
|
52
|
+
*/
|
|
53
|
+
export function loadSnippets(pi: Pick<ExtensionAPI, "getCommands">): Snippet[] {
|
|
107
54
|
const seen = new Map<string, Snippet>();
|
|
108
|
-
const excludedNames = new Set<string>();
|
|
109
55
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
BUNDLED_PROMPTS_DIR,
|
|
115
|
-
join(getAgentDir(), "prompts"),
|
|
116
|
-
join(cwd, CONFIG_DIR_NAME, "prompts"),
|
|
117
|
-
];
|
|
118
|
-
const settingsPrompts = loadedPromptPaths ?? getSettingsPrompts(cwd, pi);
|
|
56
|
+
for (const command of pi.getCommands()) {
|
|
57
|
+
if (command.source !== "prompt") continue;
|
|
58
|
+
const path = resolve(command.sourceInfo.path);
|
|
59
|
+
if (!path.toLowerCase().endsWith(".md") || seen.has(path)) continue;
|
|
119
60
|
|
|
120
|
-
|
|
121
|
-
if (!existsSync(dir)) return;
|
|
122
|
-
let files: string[];
|
|
61
|
+
let raw: string;
|
|
123
62
|
try {
|
|
124
|
-
|
|
63
|
+
raw = readFileSync(path, "utf-8");
|
|
125
64
|
} catch (error) {
|
|
126
|
-
if (!(error instanceof Error) || !("code" in error)) throw error;
|
|
127
|
-
console.error(`
|
|
128
|
-
|
|
65
|
+
if (!(error instanceof Error) || !("code" in error) || !UNREADABLE_PROMPT_CODES.has(String(error.code))) throw error;
|
|
66
|
+
console.error(`prompt-snipaste: cannot read enabled prompt ${path}: ${error.message}`);
|
|
67
|
+
continue;
|
|
129
68
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
if (!(error instanceof Error) || !("code" in error)) throw error;
|
|
139
|
-
console.error(`Failed to read prompt ${fullPath}: ${error.message}`);
|
|
140
|
-
}
|
|
69
|
+
const snippet = parseSnippet(basename(path), raw);
|
|
70
|
+
if (snippet) {
|
|
71
|
+
// Source paths, not basenames, keep same-named templates independent.
|
|
72
|
+
seen.set(path, {
|
|
73
|
+
...snippet,
|
|
74
|
+
id: path,
|
|
75
|
+
description: command.description ?? snippet.description,
|
|
76
|
+
});
|
|
141
77
|
}
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const processFile = (filePath: string) => {
|
|
145
|
-
if (!existsSync(filePath) || !filePath.endsWith(".md")) return;
|
|
146
|
-
const filename = basename(filePath);
|
|
147
|
-
if (excludedNames.has(filename)) return;
|
|
148
|
-
try {
|
|
149
|
-
const snippet = parseSnippet(filename, readFileSync(filePath, "utf-8"));
|
|
150
|
-
if (snippet) seen.set(filename, snippet);
|
|
151
|
-
} catch (error) {
|
|
152
|
-
if (!(error instanceof Error) || !("code" in error)) throw error;
|
|
153
|
-
console.error(`Failed to read prompt file ${filePath}: ${error.message}`);
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
// Collect exclusions first
|
|
158
|
-
for (const raw of settingsPrompts) {
|
|
159
|
-
if (typeof raw !== "string") continue;
|
|
160
|
-
const trimmed = raw.trim();
|
|
161
|
-
if (trimmed.startsWith("-") || trimmed.startsWith("!")) {
|
|
162
|
-
const clean = trimmed.slice(1).trim();
|
|
163
|
-
excludedNames.add(basename(clean));
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// 1. Load default prompt directories
|
|
168
|
-
for (const dir of defaultDirs) {
|
|
169
|
-
processDirectory(dir);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// 2. Load Pi's resolved prompts, or explicit settings paths without a runtime.
|
|
173
|
-
for (const raw of settingsPrompts) {
|
|
174
|
-
if (typeof raw !== "string") continue;
|
|
175
|
-
let trimmed = raw.trim();
|
|
176
|
-
if (trimmed.startsWith("-") || trimmed.startsWith("!")) continue;
|
|
177
|
-
if (trimmed.startsWith("+")) trimmed = trimmed.slice(1).trim();
|
|
178
|
-
if (!trimmed) continue;
|
|
179
|
-
|
|
180
|
-
const resolved = resolveSnippetPath(trimmed, cwd);
|
|
181
|
-
if (!existsSync(resolved)) continue;
|
|
182
|
-
|
|
183
|
-
try {
|
|
184
|
-
const stat = statSync(resolved);
|
|
185
|
-
if (stat.isDirectory()) {
|
|
186
|
-
processDirectory(resolved);
|
|
187
|
-
} else if (stat.isFile() && resolved.endsWith(".md")) {
|
|
188
|
-
processFile(resolved);
|
|
189
|
-
}
|
|
190
|
-
} catch (error) {
|
|
191
|
-
if (!(error instanceof Error) || !("code" in error)) throw error;
|
|
192
|
-
console.error(`Failed to inspect prompt path ${resolved}: ${error.message}`);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Remove any excluded items
|
|
197
|
-
for (const ex of excludedNames) {
|
|
198
|
-
seen.delete(ex);
|
|
199
78
|
}
|
|
200
79
|
|
|
201
|
-
return
|
|
80
|
+
return [...seen.values()].sort((a, b) => {
|
|
202
81
|
if (a.placement !== b.placement) return a.placement === "prepend" ? -1 : 1;
|
|
203
82
|
if (a.order !== b.order) return a.order - b.order;
|
|
204
|
-
return a.name.localeCompare(b.name);
|
|
83
|
+
return a.name.localeCompare(b.name) || a.id.localeCompare(b.id);
|
|
205
84
|
});
|
|
206
85
|
}
|
package/prompt-snipaste/types.ts
CHANGED
package/screenshot/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Searches standard and localized `Pictures/Screenshots` and OneDrive screenshot d
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
|
-
pi install npm:@rol1n/
|
|
11
|
+
pi install npm:@rol1n/pi-kit
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Included and loaded by default in `@rol1n/pi-kit`. See [resource selection](../README.md#resource-selection) to disable this entry or load only selected extensions.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* screenshot — Insert the latest Windows/WSL system screenshot path into Pi's editor.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
6
|
+
import { extname, join } from "node:path";
|
|
7
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
|
|
9
|
+
const SCREENSHOT_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"]);
|
|
10
|
+
const SKIP_USER_DIRS = /^(Public|Default|Default User|All Users|desktop\.ini)$/i;
|
|
11
|
+
|
|
12
|
+
function isExpectedFileSystemError(error: unknown): boolean {
|
|
13
|
+
return error instanceof Error && "code" in error &&
|
|
14
|
+
["ENOENT", "ENOTDIR", "EACCES", "EPERM"].includes(String(error.code));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isWindows(): boolean {
|
|
18
|
+
return process.platform === "win32";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isWSL(): boolean {
|
|
22
|
+
if (process.platform !== "linux") return false;
|
|
23
|
+
if (process.env.WSL_DISTRO_NAME || process.env.WSLENV || existsSync("/mnt/c/Users")) return true;
|
|
24
|
+
try {
|
|
25
|
+
return /microsoft|wsl/i.test(readFileSync("/proc/version", "utf-8"));
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (isExpectedFileSystemError(error)) return false;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function supportsScreenshots(): boolean {
|
|
33
|
+
return isWindows() || isWSL();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findScreenshotDirs(): string[] {
|
|
37
|
+
const dirs = new Set<string>();
|
|
38
|
+
const addIfDirectory = (path: string) => {
|
|
39
|
+
try {
|
|
40
|
+
if (existsSync(path) && statSync(path).isDirectory()) dirs.add(path);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (!isExpectedFileSystemError(error)) throw error;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (isWindows()) {
|
|
47
|
+
const profile = process.env.USERPROFILE || process.env.HOME;
|
|
48
|
+
if (profile) {
|
|
49
|
+
addIfDirectory(join(profile, "Pictures", "Screenshots"));
|
|
50
|
+
addIfDirectory(join(profile, "Pictures", "屏幕截图"));
|
|
51
|
+
addIfDirectory(join(profile, "OneDrive", "Pictures", "Screenshots"));
|
|
52
|
+
addIfDirectory(join(profile, "OneDrive", "图片", "屏幕截图"));
|
|
53
|
+
addIfDirectory(join(profile, "OneDrive", "Pictures", "屏幕截图"));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (isWSL()) {
|
|
58
|
+
const usersRoot = "/mnt/c/Users";
|
|
59
|
+
try {
|
|
60
|
+
for (const entry of readdirSync(usersRoot, { withFileTypes: true })) {
|
|
61
|
+
if (!entry.isDirectory() || SKIP_USER_DIRS.test(entry.name)) continue;
|
|
62
|
+
const profile = join(usersRoot, entry.name);
|
|
63
|
+
addIfDirectory(join(profile, "Pictures", "Screenshots"));
|
|
64
|
+
addIfDirectory(join(profile, "Pictures", "屏幕截图"));
|
|
65
|
+
addIfDirectory(join(profile, "OneDrive", "Pictures", "Screenshots"));
|
|
66
|
+
addIfDirectory(join(profile, "OneDrive", "图片", "屏幕截图"));
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (!isExpectedFileSystemError(error)) throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return [...dirs];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function findLatestScreenshot(): string | null {
|
|
77
|
+
let latestPath: string | null = null;
|
|
78
|
+
let latestMtime = -1;
|
|
79
|
+
|
|
80
|
+
for (const dir of findScreenshotDirs()) {
|
|
81
|
+
let files: string[];
|
|
82
|
+
try {
|
|
83
|
+
files = readdirSync(dir);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (isExpectedFileSystemError(error)) continue;
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const file of files) {
|
|
90
|
+
if (!SCREENSHOT_EXTENSIONS.has(extname(file).toLowerCase())) continue;
|
|
91
|
+
const path = join(dir, file);
|
|
92
|
+
try {
|
|
93
|
+
const stat = statSync(path);
|
|
94
|
+
if (stat.isFile() && stat.mtimeMs > latestMtime) {
|
|
95
|
+
latestPath = path;
|
|
96
|
+
latestMtime = stat.mtimeMs;
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (!isExpectedFileSystemError(error)) throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return latestPath;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default function (pi: ExtensionAPI) {
|
|
108
|
+
pi.registerCommand("screenshot", {
|
|
109
|
+
description: "Insert the latest Windows/WSL system screenshot path into the editor",
|
|
110
|
+
handler: async (_args, ctx: ExtensionContext) => {
|
|
111
|
+
if (!supportsScreenshots()) {
|
|
112
|
+
ctx.ui.notify("Current environment is not Windows or WSL; cannot locate Windows screenshots.", "warning");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const path = findLatestScreenshot();
|
|
117
|
+
if (!path) {
|
|
118
|
+
ctx.ui.notify("No Windows screenshots found.", "warning");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
ctx.ui.pasteToEditor(`${path} `);
|
|
123
|
+
ctx.ui.notify(`Referenced screenshot: ${path.split(/[\\/]/).pop()}`, "info");
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
+
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
8
|
+
|
|
9
|
+
assert.equal(manifest.name, "@rol1n/pi-kit");
|
|
10
|
+
assert.match(manifest.version, /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/, "Expected a release version in package.json");
|
|
11
|
+
assert.equal(manifest.type, "module");
|
|
12
|
+
assert.equal(manifest.license, "MIT");
|
|
13
|
+
assert.ok(manifest.keywords.includes("pi-package"), "Missing pi-package discovery keyword");
|
|
14
|
+
assert.ok(Array.isArray(manifest.files), "Expected a files allowlist");
|
|
15
|
+
|
|
16
|
+
// Validate the hand-maintained manifest; never generate or rewrite it.
|
|
17
|
+
const extensions = readdirSync(root, { withFileTypes: true })
|
|
18
|
+
.filter((entry) => entry.isDirectory() && existsSync(join(root, entry.name, "index.ts")))
|
|
19
|
+
.map((entry) => entry.name)
|
|
20
|
+
.sort();
|
|
21
|
+
assert.ok(extensions.length > 0, "No extension directories found");
|
|
22
|
+
assert.deepEqual(
|
|
23
|
+
[...manifest.pi.extensions].sort(),
|
|
24
|
+
extensions.map((name) => `./${name}/index.ts`),
|
|
25
|
+
"pi.extensions must declare every top-level extension exactly once",
|
|
26
|
+
);
|
|
27
|
+
for (const name of extensions) {
|
|
28
|
+
assert.ok(existsSync(join(root, name, "README.md")), `Missing ${name}/README.md`);
|
|
29
|
+
assert.ok(manifest.files.includes(name), `files must include the complete ${name} directory`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
assert.deepEqual(manifest.pi.prompts, ["./prompts"], "pi.prompts must declare the independent root prompts directory");
|
|
33
|
+
assert.ok(manifest.files.includes("prompts"), "files must include the root prompts directory");
|
|
34
|
+
assert.ok(existsSync(join(root, "prompts")), "Missing root prompts directory");
|
|
35
|
+
assert.deepEqual(manifest.pi.skills, ["./skills"], "pi.skills must declare the root skills directory");
|
|
36
|
+
assert.ok(manifest.files.includes("skills"), "files must include the root skills directory");
|
|
37
|
+
const skills = readdirSync(join(root, "skills"), { withFileTypes: true })
|
|
38
|
+
.filter((entry) => entry.isDirectory() && existsSync(join(root, "skills", entry.name, "SKILL.md")))
|
|
39
|
+
.map((entry) => entry.name);
|
|
40
|
+
assert.ok(skills.length > 0, "Expected at least one skill directory containing SKILL.md");
|
|
41
|
+
assert.deepEqual(manifest.pi.themes, []);
|
|
42
|
+
for (const name of ["@earendil-works/pi-ai", "@earendil-works/pi-agent-core", "@earendil-works/pi-coding-agent", "@earendil-works/pi-tui", "typebox"]) {
|
|
43
|
+
assert.equal(manifest.peerDependencies[name], "*", `${name} must remain a host-provided peer`);
|
|
44
|
+
assert.ok(!manifest.dependencies?.[name], `Do not install a separate copy of ${name}`);
|
|
45
|
+
}
|
|
46
|
+
for (const exclusion of ["!**/test/**", "!**/tests/**", "!**/*.{test,spec}.{ts,js,mjs,cjs}", "!**/.env", "!**/.env.*"]) {
|
|
47
|
+
assert.ok(manifest.files.includes(exclusion), `Missing package exclusion: ${exclusion}`);
|
|
48
|
+
}
|
|
49
|
+
for (const path of ["LICENSE", "README.md", "README_zh.md", "scripts/check-package.mjs"]) {
|
|
50
|
+
assert.ok(existsSync(join(root, path)), `Missing ${path}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const promptCount = readdirSync(join(root, "prompts")).filter((name) => name.endsWith(".md")).length;
|
|
54
|
+
console.log(`Checked ${manifest.name}@${manifest.version}: ${extensions.length} extensions, ${promptCount} prompts, ${skills.length} skill${skills.length === 1 ? "" : "s"}.`);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: worktree-studio
|
|
3
|
+
description: Orchestrate isolated Git-worktree experiments and compare independently implemented alternatives with real diffs and verification. Use for The Road Not Taken, competing implementation approaches, parallel worktree exploration, or accepting/rejecting agent-produced worktrees. Owns planning, confirmation, directory layout, worker delegation, evidence collection, and comparison; does not auto-integrate or discard results.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Worktree Studio — The Road Not Taken
|
|
7
|
+
|
|
8
|
+
Separate the workflow from the runtime. Git manages directories; this skill manages the experiment; a delegation tool runs each task in the specified `cwd`. `multi_harness` is one runtime, not a requirement for creating worktrees. Ordinary `subagent` or another available runtime may be used with equivalent explicit directory and lifecycle controls.
|
|
9
|
+
|
|
10
|
+
## 1. Define and confirm the experiment
|
|
11
|
+
|
|
12
|
+
- Use experiments only when evidence from genuinely viable alternatives will resolve uncertainty. Do not manufacture alternatives for a routine change.
|
|
13
|
+
- Define the common goal, concrete acceptance criteria, each approach, and the number of alternatives. Do not hard-code two workers or require different harnesses.
|
|
14
|
+
- Choose a reproducible baseline and name workers by purpose (for example `cache-codex`, `batch-pi`). Identify write overlap and dependencies.
|
|
15
|
+
- Confirm the plan with the user **before creating worktrees or launching paid workers**. Explain expected workers/models, directories, resource costs, and retained artifacts. Timeouts are not spending limits; these workers may have full filesystem permissions.
|
|
16
|
+
- Respect existing user approval without repeating questions already answered. Do not interpret approval to explore as approval to merge, commit, install dependencies, or delete results.
|
|
17
|
+
|
|
18
|
+
## 2. Inspect the baseline without disturbing changes
|
|
19
|
+
|
|
20
|
+
Use Git with argument-safe paths, for example:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
git -C <source> rev-parse --show-toplevel
|
|
24
|
+
git -C <source> rev-parse HEAD
|
|
25
|
+
git -C <source> status --porcelain=v1 --untracked-files=normal
|
|
26
|
+
git -C <source> worktree list --porcelain
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Read relevant project instructions and code. Record the exact baseline SHA, source directory, and acceptance commands in a plan file outside worker directories.
|
|
30
|
+
|
|
31
|
+
A dirty source cannot be reproduced from `HEAD`. Stop and explain the discrepancy. Ask the user to resolve the state, or explicitly approve comparing the committed baseline while leaving local changes untouched. Never automatically stash, stage, commit, reset, or copy ignored/secret files. Do not claim an experiment includes uncommitted context when it does not.
|
|
32
|
+
|
|
33
|
+
## 3. Lay out isolated worktrees
|
|
34
|
+
|
|
35
|
+
Choose a fresh experiment directory outside the source checkout and never reuse a nonempty path. Record a mapping of approach → worker name → harness/model → absolute worktree path.
|
|
36
|
+
|
|
37
|
+
For each alternative:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
git -C <source> worktree add --detach <absolute-new-path> <baseline-sha>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If creation partially fails, keep successful worktrees and report their paths. Do not force-remove them. Do not use Git locks or branches shared between workers.
|
|
44
|
+
|
|
45
|
+
Ignored dependencies/build outputs are not copied by Git. Establish what can be verified without installing anything; ask before dependency installation or version changes. A worktree is not a security sandbox.
|
|
46
|
+
|
|
47
|
+
## 4. Delegate one well-scoped task per directory
|
|
48
|
+
|
|
49
|
+
Use any suitable runtime. With `multi_harness`, each worker is an equal CLI peer (`pi`, `codex`, `claude`, or `opencode`), not a mode:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{"action":"spawn","name":"cache-codex","harness":"codex","cwd":"/absolute/experiment/cache","task":"Implement the cache approach. Baseline: <sha>. Acceptance: <criteria and commands>. Work only in this directory. Do not commit, merge, switch branches, delete worktrees, modify the source checkout, read secrets, or install dependencies without permission. Report changes, commands actually executed, results, blockers, and trade-offs."}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- Give every worker the **same goal and acceptance criteria**, with a different assigned approach where appropriate.
|
|
56
|
+
- Include explicit paths, scope, no-go operations, expected artifacts, and verification commands in the task.
|
|
57
|
+
- Native TUIs stay open after each turn. Completion arrives as a pushed notification. Do not wait in bash loops, repeatedly capture panes, or continually call `status`.
|
|
58
|
+
- Treat `harness-done "summary"` as a summary submission; settlement hooks produce the authoritative turn-completion event. A worker's success claim is not independent verification.
|
|
59
|
+
- To clarify, repair, or request more evidence, send a follow-up to the same idle worker:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{"action":"send","target":"cache-codex","task":"Run the missing acceptance check in your existing directory. Report the exact command, result, and remaining limitations; do not integrate or commit."}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Use `status` for a user-requested snapshot or diagnosis, not as a waiting loop. If a timeout or trust/permission dialog blocks work, surface it; do not silently bypass additional security controls.
|
|
66
|
+
|
|
67
|
+
## 5. Collect evidence and independently accept/reject
|
|
68
|
+
|
|
69
|
+
After notifications arrive, inspect **every** worktree. Keep machine evidence separate from worker summaries:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
git -C <worker-dir> rev-parse HEAD
|
|
73
|
+
git -C <worker-dir> status --short --untracked-files=normal
|
|
74
|
+
git -C <worker-dir> diff --no-ext-diff --stat <baseline-sha>
|
|
75
|
+
git -C <worker-dir> diff --no-ext-diff --binary <baseline-sha>
|
|
76
|
+
git -C <worker-dir> ls-files --others --exclude-standard -z
|
|
77
|
+
git -C <worker-dir> log --oneline <baseline-sha>..HEAD
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- Save tracked diffs and inspect untracked deliverables separately: ordinary `git diff` omits new untracked files. Never stage files merely to make a diff appear complete.
|
|
81
|
+
- Detect unexpected commits, branch changes, deleted files, dependency edits, out-of-scope changes, and modifications to the original checkout.
|
|
82
|
+
- Run comparable acceptance checks yourself when safe. Record command, directory, exit/result, and environment limitations. Do not mark an unrun or permission-blocked check as passed.
|
|
83
|
+
- Review correctness, maintainability, scope, performance evidence, compatibility, operational risks, and costs when reported. Do not fabricate absent pricing or infer free usage from zero/missing cost.
|
|
84
|
+
- For UI outputs, inspect desktop/mobile rendering if browser access is available; otherwise state that visual validation was not performed.
|
|
85
|
+
|
|
86
|
+
## 6. Report, recommend, and preserve
|
|
87
|
+
|
|
88
|
+
Write a comparison report in the experiment directory:
|
|
89
|
+
|
|
90
|
+
1. Goal, baseline SHA, source path, acceptance criteria.
|
|
91
|
+
2. Route/worker/harness/model/directory mapping.
|
|
92
|
+
3. Per-route implementation summary, tracked diff, untracked artifacts, unexpected commits, independent test results, and limitations.
|
|
93
|
+
4. Comparison table: accepted criteria, failures, trade-offs, evidence gaps, resource/cost observations.
|
|
94
|
+
5. Recommendation with reasons, or an explicit statement that evidence is insufficient.
|
|
95
|
+
6. Paths to preserved worktrees, runtime sessions/logs, and pending decisions.
|
|
96
|
+
|
|
97
|
+
Ask before integrating any result. Do not automatically choose a winner, merge, cherry-pick, commit, or delete worktrees.
|
|
98
|
+
|
|
99
|
+
When the user wants runtime cleanup, `multi_harness({"action":"close","target":"cache-codex"})` closes an idle TUI without deleting working files. For active work, use `stop` first. Explain that stopping a worker is not the same as removing its Git worktree. Any later removal needs separate explicit approval after artifact review.
|
package/srp-theme/README.md
CHANGED
|
@@ -5,10 +5,10 @@ Incremental Pi terminal presentation enhancement: adds a `belowEditor` last-prom
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
pi install npm:@rol1n/
|
|
8
|
+
pi install npm:@rol1n/pi-kit
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Included and loaded by default in `@rol1n/pi-kit`. See [resource selection](../README.md#resource-selection) to disable this entry or load only selected extensions.
|
|
12
12
|
|
|
13
13
|
## Commands
|
|
14
14
|
|
package/subagent/README.md
CHANGED
|
@@ -5,10 +5,10 @@ Delegate tasks to independent, full-powered Pi workers. The `subagent` tool retu
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
pi install npm:@rol1n/
|
|
8
|
+
pi install npm:@rol1n/pi-kit
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Included and loaded by default in `@rol1n/pi-kit`. See [resource selection](../README.md#resource-selection) to disable this entry or load only selected extensions. The parent and workers require the `pi` executable on PATH and their normal Pi authentication.
|
|
12
12
|
|
|
13
13
|
## Use
|
|
14
14
|
|
|
@@ -30,6 +30,6 @@ Also included in `@rol1n/pi-kit`. The parent and workers require the `pi` execut
|
|
|
30
30
|
|
|
31
31
|
Without a usable multiplexer, the worker backend falls back to a background process. Native Windows supports PowerShell launch commands and Zellij panes; tmux is for POSIX environments. Session transcripts and task artifacts are stored under `~/.pi/agent/`.
|
|
32
32
|
|
|
33
|
-
## Other delegation
|
|
33
|
+
## Other delegation tools
|
|
34
34
|
|
|
35
|
-
Use [multi-harness](https://github.com/RolinShmily/pi-kit/tree/main/multi-harness) for Codex, Claude, or OpenCode peers.
|
|
35
|
+
Use [multi-harness](https://github.com/RolinShmily/pi-kit/tree/main/multi-harness) for Codex, Claude, or OpenCode peers. It shares multiplexer helpers (`mux.ts`) within the same kit while exposing a separate tool.
|
package/tui-asr/README.md
CHANGED
|
@@ -5,10 +5,12 @@ Streaming voice dictation for Pi's terminal editor using Aliyun DashScope Parafo
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
pi install npm:@rol1n/
|
|
8
|
+
pi install npm:@rol1n/pi-kit
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Included and loaded by default in `@rol1n/pi-kit`. See [resource selection](../README.md#resource-selection) to disable this entry or load only selected extensions.
|
|
12
|
+
|
|
13
|
+
Uses `custom-providers` from the same kit; required provider registration is initialized automatically.
|
|
12
14
|
|
|
13
15
|
## Commands and shortcuts
|
|
14
16
|
|
package/web-reach/README.md
CHANGED
|
@@ -5,10 +5,10 @@ Keyless web search (`web_search` via Exa MCP) and multi-tier page extraction (`w
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
pi install npm:@rol1n/
|
|
8
|
+
pi install npm:@rol1n/pi-kit
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Included and loaded by default in `@rol1n/pi-kit`. See [resource selection](../README.md#resource-selection) to disable this entry or load only selected extensions.
|
|
12
12
|
|
|
13
13
|
## Configuration
|
|
14
14
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export function extractTitleFromHtml(html: string): string {
|
|
2
|
+
const match = html.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
3
|
+
return match?.[1]?.trim() ?? "";
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function isLikelyJSRendered(html: string): boolean {
|
|
7
|
+
const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
8
|
+
if (!bodyMatch) return false;
|
|
9
|
+
const textContent = bodyMatch[1]
|
|
10
|
+
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
11
|
+
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
|
12
|
+
.replace(/<[^>]+>/g, "")
|
|
13
|
+
.replace(/\s+/g, " ")
|
|
14
|
+
.trim();
|
|
15
|
+
const scriptCount = (html.match(/<script/gi) || []).length;
|
|
16
|
+
return textContent.length < 300 && scriptCount >= 2;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function htmlToMarkdown(html: string): string {
|
|
20
|
+
let s = html.replace(/<(script|style|noscript|svg|canvas|form|nav|footer|header|aside)[\s\S]*?<\/\1>/gi, " ");
|
|
21
|
+
s = s.replace(/<!--[\s\S]*?-->/g, " ");
|
|
22
|
+
|
|
23
|
+
// Headings
|
|
24
|
+
s = s.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, "\n\n# $1\n\n");
|
|
25
|
+
s = s.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, "\n\n## $1\n\n");
|
|
26
|
+
s = s.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, "\n\n### $1\n\n");
|
|
27
|
+
s = s.replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, "\n\n#### $1\n\n");
|
|
28
|
+
s = s.replace(/<h5[^>]*>([\s\S]*?)<\/h5>/gi, "\n\n##### $1\n\n");
|
|
29
|
+
s = s.replace(/<h6[^>]*>([\s\S]*?)<\/h6>/gi, "\n\n###### $1\n\n");
|
|
30
|
+
|
|
31
|
+
// Code blocks and inline code
|
|
32
|
+
s = s.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, "\n```\n$1\n```\n");
|
|
33
|
+
s = s.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, "\n```\n$1\n```\n");
|
|
34
|
+
s = s.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, " `$1` ");
|
|
35
|
+
|
|
36
|
+
// Blockquotes and emphasis
|
|
37
|
+
s = s.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, "\n> $1\n");
|
|
38
|
+
s = s.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, "**$2**");
|
|
39
|
+
s = s.replace(/<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi, "*$2*");
|
|
40
|
+
|
|
41
|
+
// Links
|
|
42
|
+
s = s.replace(/<a\s+[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, text) => {
|
|
43
|
+
const t = text.replace(/<[^>]*>/g, "").trim();
|
|
44
|
+
if (!t) return "";
|
|
45
|
+
if (href.startsWith("#") || href.startsWith("javascript:")) return t;
|
|
46
|
+
return `[${t}](${href})`;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Lists
|
|
50
|
+
s = s.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, "\n- $1");
|
|
51
|
+
s = s.replace(/<\/(ul|ol)>/gi, "\n\n");
|
|
52
|
+
|
|
53
|
+
// Tables
|
|
54
|
+
s = s.replace(/<tr[^>]*>([\s\S]*?)<\/tr>/gi, "\n$1 |");
|
|
55
|
+
s = s.replace(/<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi, " | $1");
|
|
56
|
+
s = s.replace(/<\/(table|thead|tbody)>/gi, "\n\n");
|
|
57
|
+
|
|
58
|
+
// Paragraphs and line breaks
|
|
59
|
+
s = s.replace(/<\/(p|div|section|article)>/gi, "\n\n");
|
|
60
|
+
s = s.replace(/<br\s*\/?>/gi, "\n");
|
|
61
|
+
|
|
62
|
+
// Strip remaining tags
|
|
63
|
+
s = s.replace(/<[^>]*>/g, " ");
|
|
64
|
+
|
|
65
|
+
// HTML entity decoding
|
|
66
|
+
s = s
|
|
67
|
+
.replace(/ /g, " ")
|
|
68
|
+
.replace(/&/g, "&")
|
|
69
|
+
.replace(/</g, "<")
|
|
70
|
+
.replace(/>/g, ">")
|
|
71
|
+
.replace(/"/g, '"')
|
|
72
|
+
.replace(/'/g, "'")
|
|
73
|
+
.replace(/…/g, "…")
|
|
74
|
+
.replace(/—/g, "—")
|
|
75
|
+
.replace(/–/g, "–");
|
|
76
|
+
|
|
77
|
+
return s
|
|
78
|
+
.split("\n")
|
|
79
|
+
.map((line) => line.trimEnd())
|
|
80
|
+
.filter((line, i, arr) => !(line === "" && arr[i - 1] === ""))
|
|
81
|
+
.join("\n")
|
|
82
|
+
.trim();
|
|
83
|
+
}
|