@pstdio/pocketcoder-remote 0.2.0 → 0.2.2
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/bin.js +17 -4
- package/dist/extension.js +17378 -0
- package/package.json +47 -49
- package/src/agentapi-events.ts +0 -104
- package/src/attachments.ts +0 -158
- package/src/bin.ts +0 -26
- package/src/client.ts +0 -344
- package/src/commands.ts +0 -149
- package/src/control-plane.ts +0 -30
- package/src/environment.ts +0 -19
- package/src/extension.ts +0 -200
- package/src/history.ts +0 -112
- package/src/launch.ts +0 -85
- package/src/renderers.ts +0 -76
- package/src/response-stream.ts +0 -43
- package/src/session-target.ts +0 -66
- package/src/status.ts +0 -121
package/package.json
CHANGED
|
@@ -1,51 +1,49 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"typescript": "5.9.3"
|
|
50
|
-
}
|
|
2
|
+
"name": "@pstdio/pocketcoder-remote",
|
|
3
|
+
"nx": {
|
|
4
|
+
"tags": [
|
|
5
|
+
"scope:remote",
|
|
6
|
+
"type:app"
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
"version": "0.2.2",
|
|
10
|
+
"private": false,
|
|
11
|
+
"description": "Pi-based terminal UI for PocketCoder workspaces.",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/pufflyai/pocketcoder.git",
|
|
17
|
+
"directory": "packages/remote"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"pocketcoder-remote": "dist/bin.js"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22.19.0"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "bun build ./src/bin.ts ./src/extension.ts --outdir ./dist --target node --external '@earendil-works/*'",
|
|
35
|
+
"start": "bun ./src/bin.ts",
|
|
36
|
+
"test": "bun test",
|
|
37
|
+
"typecheck": "tsc --project ./tsconfig.json --noEmit"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@earendil-works/pi-ai": "0.83.0",
|
|
41
|
+
"@earendil-works/pi-coding-agent": "0.83.0",
|
|
42
|
+
"@earendil-works/pi-tui": "0.83.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@pstdio/pocketcoder-sdk": "workspace:*",
|
|
46
|
+
"@types/bun": "1.3.14",
|
|
47
|
+
"typescript": "5.9.3"
|
|
48
|
+
}
|
|
51
49
|
}
|
package/src/agentapi-events.ts
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
interface MessageUpdate {
|
|
2
|
-
event: "message_update";
|
|
3
|
-
data: {
|
|
4
|
-
id: number;
|
|
5
|
-
message: string;
|
|
6
|
-
role: string;
|
|
7
|
-
time?: string;
|
|
8
|
-
};
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface StatusChange {
|
|
12
|
-
event: "status_change";
|
|
13
|
-
data: {
|
|
14
|
-
agent_type?: string;
|
|
15
|
-
status: "running" | "stable";
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface AgentError {
|
|
20
|
-
event: "agent_error";
|
|
21
|
-
data: {
|
|
22
|
-
level?: string;
|
|
23
|
-
message: string;
|
|
24
|
-
time?: string;
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export type AgentApiEvent = MessageUpdate | StatusChange | AgentError;
|
|
29
|
-
|
|
30
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
31
|
-
return typeof value === "object" && value !== null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function eventPayload(block: string): { event: string; parsed: Record<string, unknown> } | null {
|
|
35
|
-
let event = "message";
|
|
36
|
-
const data: string[] = [];
|
|
37
|
-
for (const line of block.split("\n")) {
|
|
38
|
-
if (line.startsWith(":")) continue;
|
|
39
|
-
const separator = line.indexOf(":");
|
|
40
|
-
const field = separator === -1 ? line : line.slice(0, separator);
|
|
41
|
-
const value = separator === -1 ? "" : line.slice(separator + 1).replace(/^ /, "");
|
|
42
|
-
if (field === "event") event = value;
|
|
43
|
-
if (field === "data") data.push(value);
|
|
44
|
-
}
|
|
45
|
-
if (data.length === 0) return null;
|
|
46
|
-
try {
|
|
47
|
-
const parsed = JSON.parse(data.join("\n")) as unknown;
|
|
48
|
-
return isRecord(parsed) ? { event, parsed } : null;
|
|
49
|
-
} catch {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function parseEvent(block: string): AgentApiEvent | null {
|
|
55
|
-
const payload = eventPayload(block);
|
|
56
|
-
if (!payload) return null;
|
|
57
|
-
const { event, parsed } = payload;
|
|
58
|
-
if (
|
|
59
|
-
event === "message_update" &&
|
|
60
|
-
typeof parsed.id === "number" &&
|
|
61
|
-
typeof parsed.message === "string" &&
|
|
62
|
-
typeof parsed.role === "string"
|
|
63
|
-
) {
|
|
64
|
-
return { event, data: parsed as MessageUpdate["data"] };
|
|
65
|
-
}
|
|
66
|
-
if (event === "status_change" && (parsed.status === "running" || parsed.status === "stable")) {
|
|
67
|
-
return { event, data: parsed as StatusChange["data"] };
|
|
68
|
-
}
|
|
69
|
-
if (event === "agent_error" && typeof parsed.message === "string") {
|
|
70
|
-
return { event, data: parsed as AgentError["data"] };
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export async function* readAgentApiEvents(
|
|
76
|
-
body: ReadableStream<Uint8Array>,
|
|
77
|
-
signal: AbortSignal,
|
|
78
|
-
): AsyncGenerator<AgentApiEvent> {
|
|
79
|
-
const reader = body.getReader();
|
|
80
|
-
const decoder = new TextDecoder();
|
|
81
|
-
let buffer = "";
|
|
82
|
-
const abort = () => {
|
|
83
|
-
void reader.cancel(signal.reason).catch(() => {});
|
|
84
|
-
};
|
|
85
|
-
if (signal.aborted) abort();
|
|
86
|
-
else signal.addEventListener("abort", abort, { once: true });
|
|
87
|
-
try {
|
|
88
|
-
while (!signal.aborted) {
|
|
89
|
-
const next = await reader.read();
|
|
90
|
-
if (next.done) break;
|
|
91
|
-
buffer = `${buffer}${decoder.decode(next.value, { stream: true })}`.replaceAll("\r\n", "\n");
|
|
92
|
-
let boundary = buffer.indexOf("\n\n");
|
|
93
|
-
while (boundary >= 0) {
|
|
94
|
-
const event = parseEvent(buffer.slice(0, boundary));
|
|
95
|
-
buffer = buffer.slice(boundary + 2);
|
|
96
|
-
if (event) yield event;
|
|
97
|
-
boundary = buffer.indexOf("\n\n");
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
} finally {
|
|
101
|
-
signal.removeEventListener("abort", abort);
|
|
102
|
-
reader.releaseLock();
|
|
103
|
-
}
|
|
104
|
-
}
|
package/src/attachments.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { basename, extname, isAbsolute, resolve } from "node:path";
|
|
3
|
-
import type { Context } from "@earendil-works/pi-ai";
|
|
4
|
-
import type { CommandContext, CommandRegistrar } from "./commands";
|
|
5
|
-
import type { ControlPlaneClient } from "./control-plane";
|
|
6
|
-
import type { TargetRef } from "./session-target";
|
|
7
|
-
|
|
8
|
-
// Turn-level attachment capture for the Pi remote: pasted images, explicit
|
|
9
|
-
// @path tokens, and the /attach queue all upload through the PocketCoder
|
|
10
|
-
// attachment API before the message referencing them is sent.
|
|
11
|
-
|
|
12
|
-
export const DIRECT_MODE_ATTACHMENT_ERROR =
|
|
13
|
-
"file attachments need the PocketCoder workspace API; a direct AgentAPI URL (POCKETCODER_AGENTAPI_URL) cannot accept managed uploads";
|
|
14
|
-
|
|
15
|
-
export interface TurnFile {
|
|
16
|
-
name: string;
|
|
17
|
-
mediaType: string;
|
|
18
|
-
bytes: Uint8Array;
|
|
19
|
-
localPath?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const IMAGE_EXTENSIONS: Record<string, string> = {
|
|
23
|
-
"image/gif": "gif",
|
|
24
|
-
"image/jpeg": "jpg",
|
|
25
|
-
"image/png": "png",
|
|
26
|
-
"image/svg+xml": "svg",
|
|
27
|
-
"image/webp": "webp",
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const MEDIA_TYPES: Record<string, string> = {
|
|
31
|
-
".csv": "text/csv",
|
|
32
|
-
".gif": "image/gif",
|
|
33
|
-
".html": "text/html",
|
|
34
|
-
".jpeg": "image/jpeg",
|
|
35
|
-
".jpg": "image/jpeg",
|
|
36
|
-
".json": "application/json",
|
|
37
|
-
".md": "text/markdown",
|
|
38
|
-
".pdf": "application/pdf",
|
|
39
|
-
".png": "image/png",
|
|
40
|
-
".svg": "image/svg+xml",
|
|
41
|
-
".txt": "text/plain",
|
|
42
|
-
".webp": "image/webp",
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
function lastUserContent(context: Context): Context["messages"][number]["content"] | undefined {
|
|
46
|
-
const message = context.messages.findLast((candidate) => candidate.role === "user");
|
|
47
|
-
return message?.role === "user" ? message.content : undefined;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function userTextOf(context: Context): string {
|
|
51
|
-
const content = lastUserContent(context);
|
|
52
|
-
if (content === undefined) throw new Error("local Pi did not provide a user message");
|
|
53
|
-
if (typeof content === "string") return content;
|
|
54
|
-
return content
|
|
55
|
-
.filter((part) => part.type === "text")
|
|
56
|
-
.map((part) => part.text)
|
|
57
|
-
.join("\n");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function imageParts(context: Context): TurnFile[] {
|
|
61
|
-
const content = lastUserContent(context);
|
|
62
|
-
if (content === undefined || typeof content === "string") return [];
|
|
63
|
-
return content
|
|
64
|
-
.filter((part) => part.type === "image")
|
|
65
|
-
.map((part) => ({
|
|
66
|
-
name: `pasted-image.${IMAGE_EXTENSIONS[part.mimeType] ?? "bin"}`,
|
|
67
|
-
mediaType: part.mimeType,
|
|
68
|
-
bytes: Uint8Array.from(Buffer.from(part.data, "base64")),
|
|
69
|
-
}));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Explicit `@path` tokens that resolve to a local regular file; quoted forms
|
|
73
|
-
// (`@"my file.pdf"`) support spaces.
|
|
74
|
-
export function pathTokens(text: string, cwd = process.cwd()): string[] {
|
|
75
|
-
const paths: string[] = [];
|
|
76
|
-
for (const match of text.matchAll(/@(?:"([^"]+)"|(\S+))/g)) {
|
|
77
|
-
const token = (match[1] ?? match[2]) as string;
|
|
78
|
-
const path = isAbsolute(token) ? token : resolve(cwd, token);
|
|
79
|
-
if (isFile(path) && !paths.includes(path)) paths.push(path);
|
|
80
|
-
}
|
|
81
|
-
return paths;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function isFile(path: string): boolean {
|
|
85
|
-
try {
|
|
86
|
-
return statSync(path).isFile();
|
|
87
|
-
} catch {
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function fileFromPath(path: string): TurnFile {
|
|
93
|
-
return {
|
|
94
|
-
name: basename(path),
|
|
95
|
-
mediaType: MEDIA_TYPES[extname(path).toLowerCase()] ?? "application/octet-stream",
|
|
96
|
-
bytes: new Uint8Array(readFileSync(path)),
|
|
97
|
-
localPath: path,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function collectTurnFiles(context: Context, queue: string[], cwd?: string): TurnFile[] {
|
|
102
|
-
const text = userTextOf(context);
|
|
103
|
-
const paths = [...queue];
|
|
104
|
-
for (const path of pathTokens(text, cwd)) {
|
|
105
|
-
if (!paths.includes(path)) paths.push(path);
|
|
106
|
-
}
|
|
107
|
-
return [...imageParts(context), ...paths.map(fileFromPath)];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export async function uploadTurnFiles(
|
|
111
|
-
controlPlane: ControlPlaneClient,
|
|
112
|
-
workspaceId: string,
|
|
113
|
-
files: TurnFile[],
|
|
114
|
-
): Promise<string[]> {
|
|
115
|
-
const ids: string[] = [];
|
|
116
|
-
for (const file of files) {
|
|
117
|
-
const uploaded = await controlPlane.attachments.upload(workspaceId, {
|
|
118
|
-
name: file.name,
|
|
119
|
-
mediaType: file.mediaType,
|
|
120
|
-
body: file.bytes,
|
|
121
|
-
sizeBytes: file.bytes.byteLength,
|
|
122
|
-
});
|
|
123
|
-
ids.push(uploaded.id);
|
|
124
|
-
}
|
|
125
|
-
return ids;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export interface AttachCommandDeps {
|
|
129
|
-
targets: TargetRef;
|
|
130
|
-
queue: string[];
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function registerAttachCommand(pi: CommandRegistrar, deps: AttachCommandDeps): void {
|
|
134
|
-
pi.registerCommand("attach", {
|
|
135
|
-
description: "Queue a local file to upload with the next message",
|
|
136
|
-
handler: async (args, ctx: CommandContext) => {
|
|
137
|
-
if (deps.targets.current.mode === "direct") {
|
|
138
|
-
ctx.ui.notify(DIRECT_MODE_ATTACHMENT_ERROR, "warning");
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
const token = args.trim();
|
|
142
|
-
if (!token) {
|
|
143
|
-
ctx.ui.notify("usage: /attach <path>", "info");
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
const path = isAbsolute(token) ? token : resolve(process.cwd(), token);
|
|
147
|
-
if (!isFile(path)) {
|
|
148
|
-
ctx.ui.notify(`no such file: ${token}`, "warning");
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
deps.queue.push(path);
|
|
152
|
-
ctx.ui.notify(
|
|
153
|
-
`queued ${basename(path)} (${deps.queue.length} attachment${deps.queue.length === 1 ? "" : "s"})`,
|
|
154
|
-
"info",
|
|
155
|
-
);
|
|
156
|
-
},
|
|
157
|
-
});
|
|
158
|
-
}
|
package/src/bin.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
import { resolvePiInvocation } from "./launch";
|
|
4
|
-
|
|
5
|
-
function main(): void {
|
|
6
|
-
let invocation: ReturnType<typeof resolvePiInvocation>;
|
|
7
|
-
try {
|
|
8
|
-
invocation = resolvePiInvocation({ argv: process.argv.slice(2) });
|
|
9
|
-
} catch (error) {
|
|
10
|
-
console.error(`pocketcoder-remote: ${error instanceof Error ? error.message : String(error)}`);
|
|
11
|
-
process.exit(1);
|
|
12
|
-
}
|
|
13
|
-
const child = spawn(invocation.command, invocation.args, {
|
|
14
|
-
stdio: "inherit",
|
|
15
|
-
env: invocation.env,
|
|
16
|
-
});
|
|
17
|
-
child.on("error", (error) => {
|
|
18
|
-
console.error(`pocketcoder-remote: failed to launch pi: ${error.message}`);
|
|
19
|
-
process.exit(1);
|
|
20
|
-
});
|
|
21
|
-
child.on("exit", (code, signal) => {
|
|
22
|
-
process.exit(code ?? (signal ? 1 : 0));
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
main();
|