pi-sdk-web 0.1.0
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/dist/cli.js +131 -0
- package/dist/server.js +526 -0
- package/dist/session.js +52 -0
- package/dist/ui-context.js +147 -0
- package/dist/verify-sdk.js +112 -0
- package/package.json +47 -0
- package/static/app.js +1624 -0
- package/static/index.html +54 -0
- package/static/style.css +925 -0
- package/static/vendor/marked.min.js +74 -0
package/dist/session.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Session lookup helpers for the pi-web CLI.
|
|
11
|
+
*
|
|
12
|
+
* `pi-web r <name>` semantics match pii: match by session display name
|
|
13
|
+
* (session_info entry), and if several sessions share the name, pick the
|
|
14
|
+
* most recently modified one.
|
|
15
|
+
*/
|
|
16
|
+
import { join, dirname } from "node:path";
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
18
|
+
import { SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
/** List all sessions, newest first. */
|
|
20
|
+
export async function listSessions() {
|
|
21
|
+
return SessionManager.listAll();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Find a session by display name and open it.
|
|
25
|
+
* Throws with available names when no match is found.
|
|
26
|
+
*/
|
|
27
|
+
export async function findSessionByName(name) {
|
|
28
|
+
const all = await listSessions();
|
|
29
|
+
const hits = all
|
|
30
|
+
.filter((s) => s.name === name)
|
|
31
|
+
.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
32
|
+
if (hits.length === 0) {
|
|
33
|
+
const names = [...new Set(all.map((s) => s.name).filter(Boolean))].sort();
|
|
34
|
+
const available = names.length > 0 ? `\nAvailable names: ${names.join(", ")}` : "";
|
|
35
|
+
throw new Error(`No session named '${name}'${available}`);
|
|
36
|
+
}
|
|
37
|
+
const info = hits[0];
|
|
38
|
+
return { info, sessionManager: SessionManager.open(info.path) };
|
|
39
|
+
}
|
|
40
|
+
/** Build a resource loader including Pi's built-in extensions. */
|
|
41
|
+
export async function loadBuiltinExtensions() {
|
|
42
|
+
try {
|
|
43
|
+
const entryUrl = import.meta.resolve("@earendil-works/pi-coding-agent");
|
|
44
|
+
const entryPath = fileURLToPath(entryUrl); // .../dist/index.js
|
|
45
|
+
const extPath = join(dirname(entryPath), "extensions", "index.js");
|
|
46
|
+
const mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(extPath).href)));
|
|
47
|
+
return mod.builtInExtensions ?? [];
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity theme: extensions call ui.theme.fg(...) / ui.theme.bg(...) to get
|
|
3
|
+
* ANSI-colored strings. The browser renders with CSS, so we return text
|
|
4
|
+
* unchanged (color codes are meaningless in the DOM). Keeps extensions from
|
|
5
|
+
* crashing on theme access; real colors can be added later.
|
|
6
|
+
*/
|
|
7
|
+
function createIdentityTheme() {
|
|
8
|
+
return new Proxy({}, {
|
|
9
|
+
get(_target, prop) {
|
|
10
|
+
// theme.name / theme.isDark etc. may be accessed as properties
|
|
11
|
+
if (prop === "name")
|
|
12
|
+
return "dark";
|
|
13
|
+
if (prop === "isDark")
|
|
14
|
+
return true;
|
|
15
|
+
// Everything else is a color function (fg/bg/...): return identity
|
|
16
|
+
return (text) => (typeof text === "string" ? text : "");
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export class WebUIContext {
|
|
21
|
+
pending = new Map();
|
|
22
|
+
sink;
|
|
23
|
+
identityTheme = createIdentityTheme();
|
|
24
|
+
constructor(sink) {
|
|
25
|
+
this.sink = sink;
|
|
26
|
+
}
|
|
27
|
+
/** Handle a browser `extension_ui_response` message. */
|
|
28
|
+
respond(id, response) {
|
|
29
|
+
const pending = this.pending.get(id);
|
|
30
|
+
if (!pending)
|
|
31
|
+
return false;
|
|
32
|
+
this.pending.delete(id);
|
|
33
|
+
if (pending.timer)
|
|
34
|
+
clearTimeout(pending.timer);
|
|
35
|
+
if (response.cancelled) {
|
|
36
|
+
pending.resolve(undefined);
|
|
37
|
+
}
|
|
38
|
+
else if (response.confirmed !== undefined) {
|
|
39
|
+
pending.resolve(response.confirmed);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
pending.resolve(response.value);
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
createDialog(request, defaultValue, parse) {
|
|
47
|
+
const id = crypto.randomUUID();
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
const timeoutMs = typeof request.timeout === "number" ? request.timeout : undefined;
|
|
50
|
+
const timer = timeoutMs
|
|
51
|
+
? setTimeout(() => {
|
|
52
|
+
this.pending.delete(id);
|
|
53
|
+
resolve(defaultValue);
|
|
54
|
+
}, timeoutMs)
|
|
55
|
+
: undefined;
|
|
56
|
+
this.pending.set(id, {
|
|
57
|
+
resolve: (value) => resolve(parse(value)),
|
|
58
|
+
timer,
|
|
59
|
+
});
|
|
60
|
+
this.sink({ type: "extension_ui_request", id, ...request });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// ------------------------------------------------------------------
|
|
64
|
+
// Dialogs (browser resolves via extension_ui_response)
|
|
65
|
+
// ------------------------------------------------------------------
|
|
66
|
+
select(title, options, opts) {
|
|
67
|
+
return this.createDialog({ method: "select", title, options, timeout: opts?.timeout }, undefined, (v) => (typeof v === "string" ? v : undefined));
|
|
68
|
+
}
|
|
69
|
+
confirm(title, message, opts) {
|
|
70
|
+
return this.createDialog({ method: "confirm", title, message, timeout: opts?.timeout }, false, (v) => v === true);
|
|
71
|
+
}
|
|
72
|
+
input(title, placeholder, opts) {
|
|
73
|
+
return this.createDialog({ method: "input", title, placeholder, timeout: opts?.timeout }, undefined, (v) => (typeof v === "string" ? v : undefined));
|
|
74
|
+
}
|
|
75
|
+
editor(title, prefill) {
|
|
76
|
+
return this.createDialog({ method: "editor", title, prefill }, undefined, (v) => typeof v === "string" ? v : undefined);
|
|
77
|
+
}
|
|
78
|
+
// ------------------------------------------------------------------
|
|
79
|
+
// Fire-and-forget UI events (broadcast to browser)
|
|
80
|
+
// ------------------------------------------------------------------
|
|
81
|
+
notify(message, type) {
|
|
82
|
+
this.sink({ type: "extension_ui_request", id: crypto.randomUUID(), method: "notify", message, notifyType: type });
|
|
83
|
+
}
|
|
84
|
+
setStatus(key, text) {
|
|
85
|
+
this.sink({ type: "extension_ui_request", id: crypto.randomUUID(), method: "setStatus", statusKey: key, statusText: text });
|
|
86
|
+
}
|
|
87
|
+
setTitle(title) {
|
|
88
|
+
this.sink({ type: "extension_ui_request", id: crypto.randomUUID(), method: "setTitle", title });
|
|
89
|
+
}
|
|
90
|
+
setWidget(key, content, options) {
|
|
91
|
+
this.sink({
|
|
92
|
+
type: "extension_ui_request",
|
|
93
|
+
id: crypto.randomUUID(),
|
|
94
|
+
method: "setWidget",
|
|
95
|
+
widgetKey: key,
|
|
96
|
+
widgetLines: Array.isArray(content) ? content : undefined,
|
|
97
|
+
widgetPlacement: options?.placement,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// ------------------------------------------------------------------
|
|
101
|
+
// Terminal-specific features: no-op in web mode
|
|
102
|
+
// ------------------------------------------------------------------
|
|
103
|
+
onTerminalInput() {
|
|
104
|
+
return () => { };
|
|
105
|
+
}
|
|
106
|
+
setWorkingMessage() { }
|
|
107
|
+
setWorkingVisible() { }
|
|
108
|
+
setWorkingIndicator() { }
|
|
109
|
+
setHiddenThinkingLabel() { }
|
|
110
|
+
setFooter() { }
|
|
111
|
+
setHeader() { }
|
|
112
|
+
custom() {
|
|
113
|
+
return Promise.resolve(undefined);
|
|
114
|
+
}
|
|
115
|
+
pasteToEditor() { }
|
|
116
|
+
setEditorText() { }
|
|
117
|
+
getEditorText() {
|
|
118
|
+
return "";
|
|
119
|
+
}
|
|
120
|
+
addAutocompleteProvider() { }
|
|
121
|
+
setEditorComponent() { }
|
|
122
|
+
getEditorComponent() {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
// ------------------------------------------------------------------
|
|
126
|
+
// Theme
|
|
127
|
+
// ------------------------------------------------------------------
|
|
128
|
+
get theme() {
|
|
129
|
+
return this.identityTheme;
|
|
130
|
+
}
|
|
131
|
+
getAllThemes() {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
getTheme() {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
setTheme() {
|
|
138
|
+
return { success: false, error: "Theme switching not supported in web mode" };
|
|
139
|
+
}
|
|
140
|
+
// ------------------------------------------------------------------
|
|
141
|
+
// Tool output expansion (web always shows expandable blocks)
|
|
142
|
+
// ------------------------------------------------------------------
|
|
143
|
+
getToolsExpanded() {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
setToolsExpanded() { }
|
|
147
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK link verification script (dev only).
|
|
3
|
+
*
|
|
4
|
+
* Proves that a third-party package can drive a full Pi AgentSession via the
|
|
5
|
+
* public SDK (@earendil-works/pi-coding-agent) without modifying Pi:
|
|
6
|
+
* 1. createAgentSession() -> AgentSession
|
|
7
|
+
* 2. bindExtensions() with a minimal no-op Web UI context
|
|
8
|
+
* 3. session.subscribe() -> event stream
|
|
9
|
+
* 4. session.prompt() -> LLM round-trip
|
|
10
|
+
*
|
|
11
|
+
* Run: npm run verify
|
|
12
|
+
*/
|
|
13
|
+
import { SessionManager, createAgentSession } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Minimal no-op UI context: logs what extensions would show in the UI.
|
|
16
|
+
// The real pi-sdk-web will map these to browser DOM (select/confirm/input/...).
|
|
17
|
+
// Note: extensions call ui.theme.fg etc. - real WebUIContext must provide a
|
|
18
|
+
// theme object (Pi exports initTheme/Theme utilities for this).
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
function createNoopUIContext() {
|
|
21
|
+
return {
|
|
22
|
+
select: async (title, options) => {
|
|
23
|
+
console.log(`[ui:select] ${title} ${JSON.stringify(options)}`);
|
|
24
|
+
return options[0];
|
|
25
|
+
},
|
|
26
|
+
confirm: async (title, message) => {
|
|
27
|
+
console.log(`[ui:confirm] ${title} ${message}`);
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
input: async (title, placeholder) => {
|
|
31
|
+
console.log(`[ui:input] ${title} ${placeholder ?? ""}`);
|
|
32
|
+
return undefined;
|
|
33
|
+
},
|
|
34
|
+
notify: (message, type) => {
|
|
35
|
+
console.log(`[ui:notify] ${type ?? "info"}: ${message}`);
|
|
36
|
+
},
|
|
37
|
+
onTerminalInput: () => () => { },
|
|
38
|
+
setStatus: (key, text) => {
|
|
39
|
+
if (text !== undefined)
|
|
40
|
+
console.log(`[ui:status] ${key}: ${text}`);
|
|
41
|
+
},
|
|
42
|
+
setWorkingMessage: () => { },
|
|
43
|
+
setWorkingVisible: () => { },
|
|
44
|
+
setWorkingIndicator: () => { },
|
|
45
|
+
setHiddenThinkingLabel: () => { },
|
|
46
|
+
setWidget: () => { },
|
|
47
|
+
setFooter: () => { },
|
|
48
|
+
setHeader: () => { },
|
|
49
|
+
setTitle: (title) => console.log(`[ui:title] ${title}`),
|
|
50
|
+
custom: async () => undefined,
|
|
51
|
+
pasteToEditor: () => { },
|
|
52
|
+
setEditorText: () => { },
|
|
53
|
+
getEditorText: () => "",
|
|
54
|
+
editor: async () => undefined,
|
|
55
|
+
addAutocompleteProvider: () => { },
|
|
56
|
+
setEditorComponent: () => { },
|
|
57
|
+
getEditorComponent: () => undefined,
|
|
58
|
+
get theme() {
|
|
59
|
+
return {};
|
|
60
|
+
},
|
|
61
|
+
getAllThemes: () => [],
|
|
62
|
+
getTheme: () => undefined,
|
|
63
|
+
setTheme: () => ({ success: true }),
|
|
64
|
+
getToolsExpanded: () => false,
|
|
65
|
+
setToolsExpanded: () => { },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function main() {
|
|
69
|
+
console.log("=== pi-sdk-web SDK link verification ===");
|
|
70
|
+
// 1. Create an in-memory session (no disk writes) - proves SDK works
|
|
71
|
+
const sessionManager = SessionManager.inMemory();
|
|
72
|
+
console.log("[1] createAgentSession (in-memory)...");
|
|
73
|
+
const { session } = await createAgentSession({ sessionManager });
|
|
74
|
+
console.log(` OK. model=${session.model?.provider}/${session.model?.id}`);
|
|
75
|
+
// 2. Bind extensions with no-op UI context
|
|
76
|
+
console.log("[2] bindExtensions (no-op UI context)...");
|
|
77
|
+
await session.bindExtensions({ uiContext: createNoopUIContext(), mode: "rpc" });
|
|
78
|
+
console.log(" OK.");
|
|
79
|
+
// 3. Subscribe to the event stream
|
|
80
|
+
const events = [];
|
|
81
|
+
let settled;
|
|
82
|
+
const settledPromise = new Promise((resolve) => {
|
|
83
|
+
settled = resolve;
|
|
84
|
+
});
|
|
85
|
+
session.subscribe((event) => {
|
|
86
|
+
const type = event.type;
|
|
87
|
+
if (!events.includes(type))
|
|
88
|
+
events.push(type);
|
|
89
|
+
console.log(`[event] ${type}`);
|
|
90
|
+
if (type === "agent_settled")
|
|
91
|
+
settled?.();
|
|
92
|
+
});
|
|
93
|
+
// 4. Prompt the model
|
|
94
|
+
console.log("[3] prompt: 'Reply with exactly: SDK-OK'...");
|
|
95
|
+
await session.prompt("Reply with exactly: SDK-OK");
|
|
96
|
+
const timeout = new Promise((resolve) => setTimeout(resolve, 120_000));
|
|
97
|
+
await Promise.race([settledPromise, timeout]);
|
|
98
|
+
console.log("\n=== Result ===");
|
|
99
|
+
console.log(`event types seen: ${events.join(", ")}`);
|
|
100
|
+
const ok = events.includes("message_start") && events.includes("message_end") && events.includes("agent_settled");
|
|
101
|
+
if (ok) {
|
|
102
|
+
console.log("✅ SDK LINK VERIFICATION PASSED");
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
console.log("⚠️ Incomplete event flow - inspect output above");
|
|
106
|
+
}
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
main().catch((err) => {
|
|
110
|
+
console.error("❌ Verification failed:", err);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-sdk-web",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browser Web access for Pi (AI coding agent) via the Pi SDK - standalone module, zero modification to Pi itself",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pi-web": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"static"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc -p tsconfig.json && node -e \"require('node:fs').cpSync('../static', 'static', { recursive: true })\"",
|
|
15
|
+
"dev": "tsx src/cli.ts",
|
|
16
|
+
"verify": "tsx src/verify-sdk.ts",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@earendil-works/pi-agent-core": "^0.84.2",
|
|
24
|
+
"@earendil-works/pi-coding-agent": "^0.84.2",
|
|
25
|
+
"ws": "^8.18.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^24.0.0",
|
|
29
|
+
"@types/ws": "^8.5.12",
|
|
30
|
+
"tsx": "^4.19.0",
|
|
31
|
+
"typescript": "^5.6.0"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/maxdai/pi-web.git"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"pi",
|
|
40
|
+
"pi-coding-agent",
|
|
41
|
+
"browser",
|
|
42
|
+
"web-ui",
|
|
43
|
+
"agent",
|
|
44
|
+
"coding-agent"
|
|
45
|
+
],
|
|
46
|
+
"author": "maxdai"
|
|
47
|
+
}
|