pi-feats 0.1.4 → 0.1.7
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 +90 -9
- package/extensions/api-server/README.md +21 -1
- package/extensions/api-server/application-log-store.ts +4 -2
- package/extensions/api-server/application-runtime.ts +79 -8
- package/extensions/api-server/application-store.ts +23 -10
- package/extensions/api-server/index.ts +6 -2
- package/extensions/api-server/profile-store.ts +109 -39
- package/extensions/api-server/server.ts +220 -44
- package/extensions/cli-resources.ts +144 -113
- package/extensions/cli-tui-mode.ts +7 -0
- package/extensions/context-memory/index.ts +6 -1
- package/extensions/conversation-search/index.ts +42 -0
- package/extensions/guardrails/index.ts +2 -1
- package/extensions/lib/application-context.ts +2 -0
- package/extensions/lib/builtin-tools.ts +24 -0
- package/extensions/lib/cli-tui-mode.ts +72 -0
- package/extensions/lib/context-memory.ts +6 -1
- package/extensions/lib/conversation-search.ts +33 -0
- package/extensions/lib/package-resources.ts +58 -0
- package/extensions/lib/pi-command.ts +11 -0
- package/extensions/lib/profile-sandbox.ts +14 -3
- package/extensions/lib/remote-hosts.ts +7 -5
- package/extensions/lib/workflow-evaluator.ts +113 -0
- package/extensions/lib/workflow-harness.ts +409 -43
- package/extensions/pi-console-webui/app/[section]/page.tsx +1 -1
- package/extensions/pi-console-webui/app/api/admin/models/route.ts +22 -0
- package/extensions/pi-console-webui/app/globals.css +3 -0
- package/extensions/pi-console-webui/components/application-handler-editor.tsx +1 -1
- package/extensions/pi-console-webui/components/application-logs.tsx +4 -4
- package/extensions/pi-console-webui/components/application-mappings.tsx +10 -12
- package/extensions/pi-console-webui/components/application-sessions.tsx +2 -2
- package/extensions/pi-console-webui/components/application-settings.tsx +12 -6
- package/extensions/pi-console-webui/components/chat-workspace.tsx +17 -7
- package/extensions/pi-console-webui/components/console-page.tsx +4 -0
- package/extensions/pi-console-webui/components/console-state.tsx +1 -1
- package/extensions/pi-console-webui/components/console.tsx +32 -11
- package/extensions/pi-console-webui/components/guardrails-panel.tsx +2 -2
- package/extensions/pi-console-webui/components/models-config-form.tsx +31 -0
- package/extensions/pi-console-webui/components/profile-model-form.tsx +54 -0
- package/extensions/pi-console-webui/components/pulse-resources.tsx +7 -6
- package/extensions/pi-console-webui/components/sequential-workflow-config.tsx +32 -0
- package/extensions/pi-console-webui/components/skill-resources.tsx +4 -3
- package/extensions/pi-console-webui/components/ui/input.tsx +1 -1
- package/extensions/pi-console-webui/index.ts +2 -1
- package/extensions/pi-console-webui/lib/admin-models.ts +38 -0
- package/extensions/pi-console-webui/lib/page-metadata.ts +4 -1
- package/extensions/pi-console-webui/next.config.js +1 -0
- package/extensions/profile-ssh.ts +274 -0
- package/extensions/profiles.ts +69 -25
- package/extensions/pulse/index.ts +148 -16
- package/extensions/pulse/store.ts +132 -18
- package/extensions/sequential-workflow.ts +121 -79
- package/extensions/skill-sources/store.ts +29 -10
- package/package.json +21 -9
- package/scripts/build-console-webui.mjs +7 -0
- package/scripts/console-webui-cache.mjs +79 -0
- package/scripts/ensure-console-build.mjs +43 -0
- package/skills/profile-ssh/SKILL.md +65 -0
- package/skills/sequential-workflow/SKILL.md +4 -3
- package/themes/legacy-dark.json +85 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { cp, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { dirname, join, relative } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
export const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
10
|
+
export const webuiRoot = join(packageRoot, "extensions", "pi-console-webui");
|
|
11
|
+
const agentRoot = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
12
|
+
export const cacheRoot = process.env.PI_FEATS_WEB_BUILD_CACHE_DIR ?? join(agentRoot, "cache", "pi-feats", "console-webui");
|
|
13
|
+
const cacheBuild = join(cacheRoot, "next-build");
|
|
14
|
+
const metadataPath = join(cacheRoot, "metadata.json");
|
|
15
|
+
const targetBuild = join(webuiRoot, ".next");
|
|
16
|
+
|
|
17
|
+
async function sourceHash(path) {
|
|
18
|
+
const hash = createHash("sha256");
|
|
19
|
+
const visit = async (directory) => {
|
|
20
|
+
for (const entry of (await readdir(directory, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
21
|
+
if ([".next", "node_modules"].includes(entry.name)) continue;
|
|
22
|
+
const fullPath = join(directory, entry.name);
|
|
23
|
+
if (entry.isDirectory()) await visit(fullPath);
|
|
24
|
+
else if (entry.isFile()) {
|
|
25
|
+
hash.update(relative(webuiRoot, fullPath));
|
|
26
|
+
hash.update(await readFile(fullPath));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
await visit(path);
|
|
31
|
+
return hash.digest("hex");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const gitCommit = () => {
|
|
35
|
+
try { return execFileSync("git", ["rev-parse", "HEAD"], { cwd: packageRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); }
|
|
36
|
+
catch { return undefined; }
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export async function buildMetadata() {
|
|
40
|
+
const next = JSON.parse(await readFile(join(packageRoot, "node_modules", "next", "package.json"), "utf8"));
|
|
41
|
+
return { sourceHash: await sourceHash(webuiRoot), nodeVersion: process.version, nextVersion: next.version, commit: gitCommit(), builtAt: new Date().toISOString() };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function loadCache() {
|
|
45
|
+
try { return JSON.parse(await readFile(metadataPath, "utf8")); }
|
|
46
|
+
catch { return undefined; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function cacheStatus() {
|
|
50
|
+
const metadata = await buildMetadata();
|
|
51
|
+
const cached = await loadCache();
|
|
52
|
+
if (!cached || !existsSync(join(cacheBuild, "BUILD_ID"))) return { state: "missing", metadata };
|
|
53
|
+
if (cached.nodeVersion !== metadata.nodeVersion || cached.nextVersion !== metadata.nextVersion) return { state: "incompatible", metadata, cached };
|
|
54
|
+
if (cached.sourceHash !== metadata.sourceHash) return { state: "changed", metadata, cached };
|
|
55
|
+
return { state: "compatible", metadata, cached };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function restoreCachedBuild() {
|
|
59
|
+
if (!existsSync(join(cacheBuild, "BUILD_ID"))) return false;
|
|
60
|
+
await rm(targetBuild, { recursive: true, force: true });
|
|
61
|
+
await cp(cacheBuild, targetBuild, { recursive: true });
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function saveBuildCache(metadata) {
|
|
66
|
+
if (!metadata) metadata = await buildMetadata();
|
|
67
|
+
if (!existsSync(join(targetBuild, "BUILD_ID"))) throw new Error("Next.js completed without producing .next/BUILD_ID.");
|
|
68
|
+
const staging = join(cacheRoot, `next-build-${process.pid}-${Date.now()}`);
|
|
69
|
+
await mkdir(cacheRoot, { recursive: true });
|
|
70
|
+
await rm(staging, { recursive: true, force: true });
|
|
71
|
+
await cp(targetBuild, staging, { recursive: true });
|
|
72
|
+
await rm(cacheBuild, { recursive: true, force: true });
|
|
73
|
+
await rename(staging, cacheBuild);
|
|
74
|
+
await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function clearBuildCache() {
|
|
78
|
+
await rm(cacheRoot, { recursive: true, force: true });
|
|
79
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { clearBuildCache, cacheStatus, packageRoot, restoreCachedBuild } from "./console-webui-cache.mjs";
|
|
4
|
+
|
|
5
|
+
const mode = process.env.PI_FEATS_BUILD_WEB;
|
|
6
|
+
if (process.env.PI_FEATS_CLEAR_WEB_CACHE === "1") {
|
|
7
|
+
await clearBuildCache();
|
|
8
|
+
console.log("Cleared the cached Pi Console WebUI build.");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const build = () => {
|
|
12
|
+
const result = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", ["run", "build:web"], { cwd: packageRoot, stdio: "inherit" });
|
|
13
|
+
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const prompt = async (message) => {
|
|
17
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
18
|
+
const answer = (await readline.question(message)).trim().toLowerCase();
|
|
19
|
+
readline.close();
|
|
20
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
if (mode === "1") {
|
|
24
|
+
console.log("Rebuilding Pi Console WebUI (PI_FEATS_BUILD_WEB=1).");
|
|
25
|
+
build();
|
|
26
|
+
} else if (mode === "0") {
|
|
27
|
+
console.log("Skipping Pi Console WebUI rebuild (PI_FEATS_BUILD_WEB=0).");
|
|
28
|
+
} else {
|
|
29
|
+
const status = await cacheStatus();
|
|
30
|
+
if (status.state === "compatible" && await restoreCachedBuild()) {
|
|
31
|
+
console.log("Restored cached Pi Console WebUI build; no frontend changes were detected.");
|
|
32
|
+
} else {
|
|
33
|
+
const message = status.state === "changed"
|
|
34
|
+
? "Pi Console WebUI changed in this update.\n\nRebuild now? [Y/n]\n y Rebuild now (recommended)\n n Keep the previous Console build\n> "
|
|
35
|
+
: "No compatible Pi Console WebUI build is available.\n\nBuild now? [Y/n]\n> ";
|
|
36
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || await prompt(message)) {
|
|
37
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) console.log("Rebuilding Pi Console WebUI in a non-interactive install.");
|
|
38
|
+
build();
|
|
39
|
+
} else {
|
|
40
|
+
console.log("Skipped Pi Console WebUI rebuild. The Console may not match this package version until you run `npm run build:web`.");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: profile-ssh
|
|
3
|
+
description: Use when connecting to hosts over SSH or when the user asks to register an SSH host for a Pi profile. Covers ordinary SSH plus the optional profile-scoped SSH configuration created by `pi ssh add`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Profile SSH
|
|
7
|
+
|
|
8
|
+
Use SSH normally unless a profile-scoped host is relevant. Profile SSH is an additional access mechanism; it does not replace the user's existing `~/.ssh/config`, SSH agent, global keys, or direct `ssh user@host` access.
|
|
9
|
+
|
|
10
|
+
## Choose the access method
|
|
11
|
+
|
|
12
|
+
1. **Ordinary SSH first** when the user gives a host, username, global alias, or has existing SSH credentials:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
ssh user@host
|
|
16
|
+
ssh existing-global-alias
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Respect the user's normal SSH configuration and agent. Do not assume a profile SSH host exists.
|
|
20
|
+
|
|
21
|
+
2. **Profile-scoped SSH** when the user refers to a host registered with `pi ssh add`, `pi profile <profile> ssh add`, or asks to use the current profile's SSH registration.
|
|
22
|
+
|
|
23
|
+
The profile config is not `~/.ssh/config`; use it explicitly:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
ssh -F "$PI_CODING_AGENT_DIR/.ssh/config" <alias>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`$PI_CODING_AGENT_DIR` identifies the active Pi profile. This keeps profile keys, known-host entries, and aliases isolated from global SSH configuration.
|
|
30
|
+
|
|
31
|
+
## Register a profile host
|
|
32
|
+
|
|
33
|
+
Only register a host when the user asks to do so or approves the setup. Run one of:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pi ssh add <alias>
|
|
37
|
+
pi profile <profile> ssh add <alias>
|
|
38
|
+
# Supported shorthand:
|
|
39
|
+
pi profile <profile> add <alias>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The setup uses the alias as the default hostname, then asks for the IP address, port, and remote user. It shows the server fingerprint for confirmation and selects an Ed25519 key for modern servers or an RSA-4096 key for legacy OpenSSH servers. It first tests that key without password fallback. If authorization is needed, `ssh-copy-id` prompts for the remote password once; the setup only saves the host after the same key-only validation succeeds.
|
|
43
|
+
|
|
44
|
+
List or remove profile-managed hosts with:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pi ssh list
|
|
48
|
+
pi profile <profile> ssh list
|
|
49
|
+
pi ssh delete <alias>
|
|
50
|
+
pi profile <profile> ssh delete <alias>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Safe connection checks
|
|
54
|
+
|
|
55
|
+
For a non-interactive verification, prefer:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
ssh -F "$PI_CODING_AGENT_DIR/.ssh/config" \
|
|
59
|
+
-o BatchMode=yes -o ConnectTimeout=15 \
|
|
60
|
+
<alias> true
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Keep host-key verification enabled. Do **not** suggest or add `StrictHostKeyChecking=no`, and do not put passwords in shell commands, scripts, settings, prompts, or logs.
|
|
64
|
+
|
|
65
|
+
If authentication fails, report the failure succinctly. Ask the user whether they want to use their normal SSH credentials or register/profile-authorize a key with `pi ssh add`; do not attempt to discover, copy, or expose private credentials.
|
|
@@ -62,9 +62,10 @@ Do not inspect, resume, or otherwise select a persisted workflow merely because
|
|
|
62
62
|
|
|
63
63
|
- For an `action`, perform the action and call `sequential_workflow_record_result` with `phase: "action"` and a verifiable result summary.
|
|
64
64
|
- For a `collect`, request the required information from the user. After receiving it, call `sequential_workflow_record_result` with `phase: "collect"` and the received response.
|
|
65
|
-
- For a `workflow` task,
|
|
66
|
-
- If a task has `criteria`, call `sequential_workflow_evaluate`
|
|
65
|
+
- For a `workflow` task, call `sequential_workflow_create_subworkflow` with both `parentWorkflowId` and `parentTaskId` for that current task. The parent task waits automatically; do not record a result for it.
|
|
66
|
+
- If a task has `criteria`, call `sequential_workflow_evaluate` with `outcome: "accept"`, `"retry"`, or `"fail"`. Do not advance before acceptance.
|
|
67
|
+
- Use `retry` only when another execution could change the outcome. After recording evidence, use `fail` when the task cannot safely progress; this ends the workflow without advancing or consuming retry attempts.
|
|
67
68
|
- For an `evaluate`, assess the available information according to the instruction and call `sequential_workflow_evaluate`.
|
|
68
69
|
- If an evaluation rejects an `action`, correct or repeat the current action. If it rejects a `collect`, explain what is missing and request the information again. A rejected `workflow` task becomes runnable again and may create a replacement child.
|
|
69
70
|
|
|
70
|
-
`
|
|
71
|
+
`sequential_workflow_create` creates only root workflows and hydrates a focused `pending_definition`; it has no parent parameters and never creates a child. `sequential_workflow_create_subworkflow` creates the hierarchical relationship and operational dependency for a current `workflow` task. Use `sequential_workflow_status` without an ID to list this session's active workflows or with `workflowId` to inspect an owned execution. Status never changes focus. When the user requests cancellation, call `sequential_workflow_cancel` with the workflow ID. Cancellation is available across sessions in the same profile and does not require adoption. It cancels only that workflow; a cancelled child is surfaced to its parent for evaluation and descendants are not cancelled automatically. `/workflow-cancel <workflowId>` remains available as an explicit user control.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
|
3
|
+
"name": "legacy-dark",
|
|
4
|
+
"vars": {
|
|
5
|
+
"cyan": "#00d7ff",
|
|
6
|
+
"blue": "#5f87ff",
|
|
7
|
+
"green": "#b5bd68",
|
|
8
|
+
"red": "#cc6666",
|
|
9
|
+
"yellow": "#ffff00",
|
|
10
|
+
"text": "#d4d4d4",
|
|
11
|
+
"gray": "#808080",
|
|
12
|
+
"dimGray": "#666666",
|
|
13
|
+
"darkGray": "#505050",
|
|
14
|
+
"accent": "#8abeb7",
|
|
15
|
+
"selectedBg": "#3a3a4a",
|
|
16
|
+
"userMsgBg": "#343541",
|
|
17
|
+
"toolPendingBg": "#282832",
|
|
18
|
+
"toolSuccessBg": "#283228",
|
|
19
|
+
"toolErrorBg": "#3c2828",
|
|
20
|
+
"customMsgBg": "#2d2838"
|
|
21
|
+
},
|
|
22
|
+
"colors": {
|
|
23
|
+
"accent": "accent",
|
|
24
|
+
"border": "blue",
|
|
25
|
+
"borderAccent": "cyan",
|
|
26
|
+
"borderMuted": "darkGray",
|
|
27
|
+
"success": "green",
|
|
28
|
+
"error": "red",
|
|
29
|
+
"warning": "yellow",
|
|
30
|
+
"muted": "gray",
|
|
31
|
+
"dim": "dimGray",
|
|
32
|
+
"text": "text",
|
|
33
|
+
"thinkingText": "gray",
|
|
34
|
+
"selectedBg": "selectedBg",
|
|
35
|
+
"scrollbarTrack": "darkGray",
|
|
36
|
+
"scrollbarThumb": "text",
|
|
37
|
+
"searchMatchBg": "selectedBg",
|
|
38
|
+
"searchMatchText": "text",
|
|
39
|
+
"userMessageBg": "userMsgBg",
|
|
40
|
+
"userMessageText": "text",
|
|
41
|
+
"customMessageBg": "customMsgBg",
|
|
42
|
+
"customMessageText": "text",
|
|
43
|
+
"customMessageLabel": "#9575cd",
|
|
44
|
+
"toolPendingBg": "toolPendingBg",
|
|
45
|
+
"toolSuccessBg": "toolSuccessBg",
|
|
46
|
+
"toolErrorBg": "toolErrorBg",
|
|
47
|
+
"toolTitle": "text",
|
|
48
|
+
"toolOutput": "gray",
|
|
49
|
+
"mdHeading": "#f0c674",
|
|
50
|
+
"mdLink": "#81a2be",
|
|
51
|
+
"mdLinkUrl": "dimGray",
|
|
52
|
+
"mdCode": "accent",
|
|
53
|
+
"mdCodeBlock": "green",
|
|
54
|
+
"mdCodeBlockBorder": "gray",
|
|
55
|
+
"mdQuote": "gray",
|
|
56
|
+
"mdQuoteBorder": "gray",
|
|
57
|
+
"mdHr": "gray",
|
|
58
|
+
"mdListBullet": "accent",
|
|
59
|
+
"toolDiffAdded": "green",
|
|
60
|
+
"toolDiffRemoved": "red",
|
|
61
|
+
"toolDiffContext": "gray",
|
|
62
|
+
"syntaxComment": "#6A9955",
|
|
63
|
+
"syntaxKeyword": "#569CD6",
|
|
64
|
+
"syntaxFunction": "#DCDCAA",
|
|
65
|
+
"syntaxVariable": "#9CDCFE",
|
|
66
|
+
"syntaxString": "#CE9178",
|
|
67
|
+
"syntaxNumber": "#B5CEA8",
|
|
68
|
+
"syntaxType": "#4EC9B0",
|
|
69
|
+
"syntaxOperator": "#D4D4D4",
|
|
70
|
+
"syntaxPunctuation": "#D4D4D4",
|
|
71
|
+
"thinkingOff": "darkGray",
|
|
72
|
+
"thinkingMinimal": "#6e6e6e",
|
|
73
|
+
"thinkingLow": "#5f87af",
|
|
74
|
+
"thinkingMedium": "#81a2be",
|
|
75
|
+
"thinkingHigh": "#b294bb",
|
|
76
|
+
"thinkingXhigh": "#d183e8",
|
|
77
|
+
"thinkingMax": "#ff5fff",
|
|
78
|
+
"bashMode": "green"
|
|
79
|
+
},
|
|
80
|
+
"export": {
|
|
81
|
+
"pageBg": "#18181e",
|
|
82
|
+
"cardBg": "#1e1e24",
|
|
83
|
+
"infoBg": "#3c3728"
|
|
84
|
+
}
|
|
85
|
+
}
|