@fleetagent/pi-coding-agent 0.0.1 → 0.0.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/CHANGELOG.md +16 -0
- package/dist/core/agent-session.d.ts +5 -5
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +8 -11
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts +2 -2
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +2 -2
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +1 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +3 -3
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +5 -5
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +15 -8
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +1 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +1 -1
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/pi-agent.d.ts +5 -3
- package/dist/core/pi-agent.d.ts.map +1 -1
- package/dist/core/pi-agent.js +64 -18
- package/dist/core/pi-agent.js.map +1 -1
- package/dist/core/session/in-memory-session-manager.d.ts.map +1 -1
- package/dist/core/session/in-memory-session-manager.js +5 -7
- package/dist/core/session/in-memory-session-manager.js.map +1 -1
- package/dist/core/session/in-memory-session.d.ts +3 -1
- package/dist/core/session/in-memory-session.d.ts.map +1 -1
- package/dist/core/session/in-memory-session.js +5 -2
- package/dist/core/session/in-memory-session.js.map +1 -1
- package/dist/core/session/index.d.ts +2 -2
- package/dist/core/session/index.d.ts.map +1 -1
- package/dist/core/session/index.js.map +1 -1
- package/dist/core/session/jsonl-helpers.d.ts.map +1 -1
- package/dist/core/session/jsonl-helpers.js +4 -4
- package/dist/core/session/jsonl-helpers.js.map +1 -1
- package/dist/core/session/local-session-manager.d.ts.map +1 -1
- package/dist/core/session/local-session-manager.js +12 -11
- package/dist/core/session/local-session-manager.js.map +1 -1
- package/dist/core/session/local-session.d.ts +3 -1
- package/dist/core/session/local-session.d.ts.map +1 -1
- package/dist/core/session/local-session.js +7 -2
- package/dist/core/session/local-session.js.map +1 -1
- package/dist/core/session/remote-session-client.d.ts +6 -1
- package/dist/core/session/remote-session-client.d.ts.map +1 -1
- package/dist/core/session/remote-session-client.js.map +1 -1
- package/dist/core/session/remote-session-manager.d.ts.map +1 -1
- package/dist/core/session/remote-session-manager.js +28 -7
- package/dist/core/session/remote-session-manager.js.map +1 -1
- package/dist/core/session/remote-session.d.ts +3 -0
- package/dist/core/session/remote-session.d.ts.map +1 -1
- package/dist/core/session/remote-session.js +4 -1
- package/dist/core/session/remote-session.js.map +1 -1
- package/dist/core/session/session.d.ts +9 -3
- package/dist/core/session/session.d.ts.map +1 -1
- package/dist/core/session/session.js +64 -10
- package/dist/core/session/session.js.map +1 -1
- package/dist/core/session/stores/in-memory-session-store.d.ts +6 -14
- package/dist/core/session/stores/in-memory-session-store.d.ts.map +1 -1
- package/dist/core/session/stores/in-memory-session-store.js +8 -34
- package/dist/core/session/stores/in-memory-session-store.js.map +1 -1
- package/dist/core/session/stores/jsonl-session-store.d.ts +14 -14
- package/dist/core/session/stores/jsonl-session-store.d.ts.map +1 -1
- package/dist/core/session/stores/jsonl-session-store.js +153 -162
- package/dist/core/session/stores/jsonl-session-store.js.map +1 -1
- package/dist/core/session/stores/remote-session-store.d.ts +4 -6
- package/dist/core/session/stores/remote-session-store.d.ts.map +1 -1
- package/dist/core/session/stores/remote-session-store.js +18 -30
- package/dist/core/session/stores/remote-session-store.js.map +1 -1
- package/dist/core/session/stores/session-store.d.ts +1 -15
- package/dist/core/session/stores/session-store.d.ts.map +1 -1
- package/dist/core/session/stores/session-store.js.map +1 -1
- package/dist/core/session-cwd.d.ts +2 -2
- package/dist/core/session-cwd.d.ts.map +1 -1
- package/dist/core/session-cwd.js +5 -5
- package/dist/core/session-cwd.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +39 -37
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/extensions.md +35 -32
- package/docs/index.md +1 -1
- package/docs/sdk.md +2 -0
- package/docs/session-format.md +21 -21
- package/docs/sessions.md +2 -2
- package/docs/tui.md +1 -1
- package/examples/README.md +3 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/auto-commit-on-exit.ts +1 -1
- package/examples/extensions/bookmark.ts +3 -3
- package/examples/extensions/confirm-destructive.ts +1 -1
- package/examples/extensions/custom-compaction.ts +1 -1
- package/examples/extensions/custom-footer.ts +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/git-checkpoint.ts +1 -1
- package/examples/extensions/handoff.ts +2 -2
- package/examples/extensions/plan-mode/index.ts +1 -1
- package/examples/extensions/preset.ts +1 -1
- package/examples/extensions/qna.ts +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/snake.ts +1 -1
- package/examples/extensions/space-invaders.ts +1 -1
- package/examples/extensions/summarize.ts +1 -1
- package/examples/extensions/tic-tac-toe.ts +1 -1
- package/examples/extensions/todo.ts +1 -1
- package/examples/extensions/tools.ts +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/remote-session-server/README.md +66 -0
- package/examples/remote-session-server/server.ts +359 -0
- package/examples/sdk/11-sessions.ts +3 -3
- package/examples/sdk/13-session-runtime.ts +6 -6
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Remote session storage server
|
|
2
|
+
|
|
3
|
+
Example Hono server for pi remote sessions. It stores each session as a JSONL file for easy inspection and uses a static bearer token.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
PI_REMOTE_SESSION_TOKEN=dev-token \
|
|
9
|
+
PI_REMOTE_SESSION_DIR=/tmp/pi-remote-sessions \
|
|
10
|
+
npx tsx packages/coding-agent/examples/remote-session-server/server.ts
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The server listens on `http://localhost:8787` by default. Override with `PORT`. Requests and session operations are logged to stdout.
|
|
14
|
+
|
|
15
|
+
## Use from pi CLI
|
|
16
|
+
|
|
17
|
+
From the repo root:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
PI_REMOTE_SESSION_BASE_URL=http://localhost:8787 \
|
|
21
|
+
PI_REMOTE_SESSION_TOKEN=dev-token \
|
|
22
|
+
./pi-test.sh
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Equivalent flags:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
./pi-test.sh --remote-session-base-url http://localhost:8787 --remote-session-token dev-token
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Optional project id:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
PI_REMOTE_PROJECT_ID=my-project \
|
|
35
|
+
PI_REMOTE_SESSION_BASE_URL=http://localhost:8787 \
|
|
36
|
+
PI_REMOTE_SESSION_TOKEN=dev-token \
|
|
37
|
+
./pi-test.sh
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Use from the SDK
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { PiAgent, RemoteSessionManager } from "@fleetagent/pi-coding-agent";
|
|
44
|
+
|
|
45
|
+
const sessionManager = new RemoteSessionManager({
|
|
46
|
+
baseUrl: "http://localhost:8787",
|
|
47
|
+
token: "dev-token",
|
|
48
|
+
cwd: process.cwd(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const pi = await PiAgent.create({ sessionManager });
|
|
52
|
+
await pi.prompt("hello from a remote session");
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Sessions are stored as `<session-id>.jsonl` under `PI_REMOTE_SESSION_DIR`.
|
|
56
|
+
|
|
57
|
+
## API covered
|
|
58
|
+
|
|
59
|
+
- `POST /v1/sessions`
|
|
60
|
+
- `GET /v1/sessions`
|
|
61
|
+
- `GET /v1/sessions/recent`
|
|
62
|
+
- `GET /v1/sessions/:id`
|
|
63
|
+
- `POST /v1/sessions/:id/entries`
|
|
64
|
+
- `PUT /v1/sessions/:id/snapshot`
|
|
65
|
+
- `POST /v1/sessions/:id/fork`
|
|
66
|
+
- `POST /v1/sessions/import-jsonl`
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
CURRENT_SESSION_VERSION,
|
|
6
|
+
type FileEntry,
|
|
7
|
+
type SessionEntry,
|
|
8
|
+
type SessionHeader,
|
|
9
|
+
type SessionInfo,
|
|
10
|
+
} from "@fleetagent/pi-coding-agent";
|
|
11
|
+
import { serve } from "@hono/node-server";
|
|
12
|
+
import { Hono } from "hono";
|
|
13
|
+
|
|
14
|
+
interface CreateSessionRequest {
|
|
15
|
+
id?: string;
|
|
16
|
+
cwd: string;
|
|
17
|
+
projectId?: string;
|
|
18
|
+
parentSession?: string;
|
|
19
|
+
metadata?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface AppendEntriesRequest {
|
|
23
|
+
baseEtag?: string;
|
|
24
|
+
entries: FileEntry[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ReplaceSnapshotRequest {
|
|
28
|
+
baseEtag?: string;
|
|
29
|
+
entries: FileEntry[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ForkSessionRequest {
|
|
33
|
+
cwd: string;
|
|
34
|
+
projectId?: string;
|
|
35
|
+
leafId?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ImportJsonlRequest {
|
|
39
|
+
cwd: string;
|
|
40
|
+
projectId?: string;
|
|
41
|
+
sourceName?: string;
|
|
42
|
+
entries: FileEntry[];
|
|
43
|
+
metadata?: Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface RemoteSessionSnapshot {
|
|
47
|
+
reference: string;
|
|
48
|
+
id: string;
|
|
49
|
+
version?: number;
|
|
50
|
+
entries: FileEntry[];
|
|
51
|
+
etag: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const port = Number.parseInt(process.env.PORT ?? "8787", 10);
|
|
55
|
+
const token = process.env.PI_REMOTE_SESSION_TOKEN ?? "dev-token";
|
|
56
|
+
const dataDir = process.env.PI_REMOTE_SESSION_DIR ?? join(process.cwd(), ".pi-remote-sessions");
|
|
57
|
+
const app = new Hono();
|
|
58
|
+
|
|
59
|
+
function log(message: string, details?: Record<string, unknown>): void {
|
|
60
|
+
const suffix = details ? ` ${JSON.stringify(details)}` : "";
|
|
61
|
+
console.log(`[${new Date().toISOString()}] ${message}${suffix}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
65
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isSessionId(value: string): boolean {
|
|
69
|
+
return /^[A-Za-z0-9._-]+$/.test(value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function stripRemotePrefix(reference: string): string {
|
|
73
|
+
return reference.startsWith("remote:") ? reference.slice("remote:".length) : reference;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function assertSessionId(value: string): string {
|
|
77
|
+
const id = stripRemotePrefix(value);
|
|
78
|
+
if (!isSessionId(id)) {
|
|
79
|
+
throw new Error(`Invalid session id: ${value}`);
|
|
80
|
+
}
|
|
81
|
+
return id;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function createSessionId(): string {
|
|
85
|
+
return randomUUID();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sessionPath(sessionId: string): string {
|
|
89
|
+
return join(dataDir, `${assertSessionId(sessionId)}.jsonl`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function referenceFor(sessionId: string): string {
|
|
93
|
+
return `remote:${sessionId}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function etagFor(entries: FileEntry[]): string {
|
|
97
|
+
return createHash("sha256")
|
|
98
|
+
.update(entries.map((entry) => JSON.stringify(entry)).join("\n"))
|
|
99
|
+
.digest("hex");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function writeJsonl(entries: FileEntry[]): string {
|
|
103
|
+
return `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseJsonl(content: string): FileEntry[] {
|
|
107
|
+
const entries: FileEntry[] = [];
|
|
108
|
+
for (const line of content.split("\n")) {
|
|
109
|
+
if (!line.trim()) continue;
|
|
110
|
+
entries.push(JSON.parse(line) as FileEntry);
|
|
111
|
+
}
|
|
112
|
+
return entries;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function loadEntries(sessionId: string): Promise<FileEntry[]> {
|
|
116
|
+
return parseJsonl(await readFile(sessionPath(sessionId), "utf8"));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function saveEntries(sessionId: string, entries: FileEntry[]): Promise<void> {
|
|
120
|
+
await mkdir(dataDir, { recursive: true });
|
|
121
|
+
await writeFile(sessionPath(sessionId), writeJsonl(entries), "utf8");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function snapshotFor(sessionId: string, entries: FileEntry[]): RemoteSessionSnapshot {
|
|
125
|
+
const header = entries[0];
|
|
126
|
+
return {
|
|
127
|
+
reference: referenceFor(sessionId),
|
|
128
|
+
id: sessionId,
|
|
129
|
+
version: header?.type === "session" ? header.version : undefined,
|
|
130
|
+
entries,
|
|
131
|
+
etag: etagFor(entries),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function validateBaseEtag(entries: FileEntry[], baseEtag: string | undefined): boolean {
|
|
136
|
+
return baseEtag === undefined || baseEtag === etagFor(entries);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function makeHeader(id: string, cwd: string, parentSession?: string): SessionHeader {
|
|
140
|
+
return {
|
|
141
|
+
type: "session",
|
|
142
|
+
version: CURRENT_SESSION_VERSION,
|
|
143
|
+
id,
|
|
144
|
+
timestamp: new Date().toISOString(),
|
|
145
|
+
cwd,
|
|
146
|
+
parentSession,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getBranch(entries: FileEntry[], leafId: string | undefined): SessionEntry[] {
|
|
151
|
+
const sessionEntries = entries.filter((entry): entry is SessionEntry => entry.type !== "session");
|
|
152
|
+
if (leafId === undefined) {
|
|
153
|
+
return sessionEntries.filter((entry) => entry.type !== "label");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const byId = new Map(sessionEntries.map((entry) => [entry.id, entry]));
|
|
157
|
+
const path: SessionEntry[] = [];
|
|
158
|
+
let current: string | null | undefined = leafId;
|
|
159
|
+
while (current) {
|
|
160
|
+
const entry = byId.get(current);
|
|
161
|
+
if (!entry) {
|
|
162
|
+
throw new Error(`Entry ${leafId} not found`);
|
|
163
|
+
}
|
|
164
|
+
path.unshift(entry);
|
|
165
|
+
current = entry.parentId;
|
|
166
|
+
}
|
|
167
|
+
return path.filter((entry) => entry.type !== "label");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function forkEntries(sourceEntries: FileEntry[], header: SessionHeader, leafId: string | undefined): FileEntry[] {
|
|
171
|
+
const path = getBranch(sourceEntries, leafId);
|
|
172
|
+
const pathIds = new Set(path.map((entry) => entry.id));
|
|
173
|
+
const labelEntries = sourceEntries.filter(
|
|
174
|
+
(entry): entry is SessionEntry & { type: "label"; targetId: string } =>
|
|
175
|
+
entry.type === "label" && pathIds.has(entry.targetId),
|
|
176
|
+
);
|
|
177
|
+
return [header, ...structuredClone(path), ...structuredClone(labelEntries)];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isMessageWithContent(value: unknown): value is { role?: string; content?: unknown } {
|
|
181
|
+
return isRecord(value) && "content" in value;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function extractText(value: unknown): string {
|
|
185
|
+
if (typeof value === "string") return value;
|
|
186
|
+
if (!Array.isArray(value)) return "";
|
|
187
|
+
return value
|
|
188
|
+
.filter(
|
|
189
|
+
(part): part is { type: string; text: string } =>
|
|
190
|
+
isRecord(part) && part.type === "text" && typeof part.text === "string",
|
|
191
|
+
)
|
|
192
|
+
.map((part) => part.text)
|
|
193
|
+
.join(" ");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function sessionInfo(sessionId: string, entries: FileEntry[], modified: Date): SessionInfo | null {
|
|
197
|
+
const header = entries[0];
|
|
198
|
+
if (!header || header.type !== "session") return null;
|
|
199
|
+
|
|
200
|
+
let name: string | undefined;
|
|
201
|
+
let messageCount = 0;
|
|
202
|
+
let firstMessage = "";
|
|
203
|
+
const allMessages: string[] = [];
|
|
204
|
+
for (const entry of entries) {
|
|
205
|
+
if (entry.type === "session_info") {
|
|
206
|
+
name = entry.name?.trim() || undefined;
|
|
207
|
+
}
|
|
208
|
+
if (entry.type !== "message") continue;
|
|
209
|
+
messageCount++;
|
|
210
|
+
const message = entry.message;
|
|
211
|
+
if (!isMessageWithContent(message)) continue;
|
|
212
|
+
if (message.role !== "user" && message.role !== "assistant") continue;
|
|
213
|
+
const text = extractText(message.content);
|
|
214
|
+
if (!text) continue;
|
|
215
|
+
allMessages.push(text);
|
|
216
|
+
if (!firstMessage && message.role === "user") {
|
|
217
|
+
firstMessage = text;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
reference: referenceFor(sessionId),
|
|
223
|
+
path: referenceFor(sessionId),
|
|
224
|
+
id: sessionId,
|
|
225
|
+
cwd: header.cwd,
|
|
226
|
+
name,
|
|
227
|
+
parentSessionPath: header.parentSession,
|
|
228
|
+
created: new Date(header.timestamp),
|
|
229
|
+
modified,
|
|
230
|
+
messageCount,
|
|
231
|
+
firstMessage: firstMessage || "(no messages)",
|
|
232
|
+
allMessagesText: allMessages.join(" "),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function authorized(authHeader: string | undefined): boolean {
|
|
237
|
+
const prefix = "Bearer ";
|
|
238
|
+
if (!authHeader?.startsWith(prefix)) return false;
|
|
239
|
+
const received = Buffer.from(authHeader.slice(prefix.length));
|
|
240
|
+
const expected = Buffer.from(token);
|
|
241
|
+
return received.length === expected.length && timingSafeEqual(received, expected);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
app.use("/v1/*", async (ctx, next) => {
|
|
245
|
+
const started = Date.now();
|
|
246
|
+
const method = ctx.req.method;
|
|
247
|
+
const path = new URL(ctx.req.url).pathname;
|
|
248
|
+
log("request", { method, path });
|
|
249
|
+
|
|
250
|
+
if (!authorized(ctx.req.header("Authorization"))) {
|
|
251
|
+
log("response", { method, path, status: 401, durationMs: Date.now() - started });
|
|
252
|
+
return ctx.text("Unauthorized", 401);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await next();
|
|
256
|
+
log("response", { method, path, status: ctx.res.status, durationMs: Date.now() - started });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
app.get("/health", (ctx) => ctx.json({ ok: true, dataDir }));
|
|
260
|
+
|
|
261
|
+
app.post("/v1/sessions", async (ctx) => {
|
|
262
|
+
const body = (await ctx.req.json()) as CreateSessionRequest;
|
|
263
|
+
const id = assertSessionId(body.id ?? createSessionId());
|
|
264
|
+
const entries: FileEntry[] = [makeHeader(id, body.cwd, body.parentSession)];
|
|
265
|
+
log("create session", { id, cwd: body.cwd, parentSession: body.parentSession, projectId: body.projectId });
|
|
266
|
+
await saveEntries(id, entries);
|
|
267
|
+
return ctx.json(snapshotFor(id, entries));
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
app.get("/v1/sessions", async (ctx) => {
|
|
271
|
+
await mkdir(dataDir, { recursive: true });
|
|
272
|
+
const files = (await readdir(dataDir)).filter((file) => file.endsWith(".jsonl"));
|
|
273
|
+
const sessions: SessionInfo[] = [];
|
|
274
|
+
for (const file of files) {
|
|
275
|
+
const id = file.slice(0, -".jsonl".length);
|
|
276
|
+
const path = sessionPath(id);
|
|
277
|
+
const entries = parseJsonl(await readFile(path, "utf8"));
|
|
278
|
+
const info = sessionInfo(id, entries, (await stat(path)).mtime);
|
|
279
|
+
if (info) sessions.push(info);
|
|
280
|
+
}
|
|
281
|
+
log("list sessions", { count: sessions.length });
|
|
282
|
+
return ctx.json({ sessions: sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime()) });
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
app.get("/v1/sessions/recent", async (ctx) => {
|
|
286
|
+
await mkdir(dataDir, { recursive: true });
|
|
287
|
+
const files = (await readdir(dataDir)).filter((file) => file.endsWith(".jsonl"));
|
|
288
|
+
let recent: { id: string; modified: Date } | undefined;
|
|
289
|
+
for (const file of files) {
|
|
290
|
+
const id = file.slice(0, -".jsonl".length);
|
|
291
|
+
const modified = (await stat(sessionPath(id))).mtime;
|
|
292
|
+
if (!recent || modified > recent.modified) {
|
|
293
|
+
recent = { id, modified };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (!recent) return ctx.text("No sessions", 404);
|
|
297
|
+
const entries = await loadEntries(recent.id);
|
|
298
|
+
log("recent session", { id: recent.id, entries: entries.length });
|
|
299
|
+
return ctx.json(snapshotFor(recent.id, entries));
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
app.get("/v1/sessions/:id", async (ctx) => {
|
|
303
|
+
const id = assertSessionId(ctx.req.param("id"));
|
|
304
|
+
const entries = await loadEntries(id);
|
|
305
|
+
log("open session", { id, entries: entries.length });
|
|
306
|
+
return ctx.json(snapshotFor(id, entries));
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
app.post("/v1/sessions/:id/entries", async (ctx) => {
|
|
310
|
+
const id = assertSessionId(ctx.req.param("id"));
|
|
311
|
+
const body = (await ctx.req.json()) as AppendEntriesRequest;
|
|
312
|
+
const entries = await loadEntries(id);
|
|
313
|
+
if (!validateBaseEtag(entries, body.baseEtag)) return ctx.text("ETag mismatch", 409);
|
|
314
|
+
const nextEntries = [...entries, ...body.entries];
|
|
315
|
+
log("append entries", {
|
|
316
|
+
id,
|
|
317
|
+
entries: body.entries.length,
|
|
318
|
+
totalEntries: nextEntries.length,
|
|
319
|
+
baseEtag: body.baseEtag,
|
|
320
|
+
});
|
|
321
|
+
await saveEntries(id, nextEntries);
|
|
322
|
+
return ctx.json({ accepted: body.entries.length, etag: etagFor(nextEntries) });
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
app.put("/v1/sessions/:id/snapshot", async (ctx) => {
|
|
326
|
+
const id = assertSessionId(ctx.req.param("id"));
|
|
327
|
+
const body = (await ctx.req.json()) as ReplaceSnapshotRequest;
|
|
328
|
+
const entries = await loadEntries(id);
|
|
329
|
+
if (!validateBaseEtag(entries, body.baseEtag)) return ctx.text("ETag mismatch", 409);
|
|
330
|
+
log("replace snapshot", { id, entries: body.entries.length, baseEtag: body.baseEtag });
|
|
331
|
+
await saveEntries(id, body.entries);
|
|
332
|
+
return ctx.json({ etag: etagFor(body.entries) });
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
app.post("/v1/sessions/:id/fork", async (ctx) => {
|
|
336
|
+
const sourceId = assertSessionId(ctx.req.param("id"));
|
|
337
|
+
const body = (await ctx.req.json()) as ForkSessionRequest;
|
|
338
|
+
const forkId = createSessionId();
|
|
339
|
+
const sourceEntries = await loadEntries(sourceId);
|
|
340
|
+
const entries = forkEntries(sourceEntries, makeHeader(forkId, body.cwd, referenceFor(sourceId)), body.leafId);
|
|
341
|
+
log("fork session", { sourceId, forkId, leafId: body.leafId, entries: entries.length, projectId: body.projectId });
|
|
342
|
+
await saveEntries(forkId, entries);
|
|
343
|
+
return ctx.json(snapshotFor(forkId, entries));
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
app.post("/v1/sessions/import-jsonl", async (ctx) => {
|
|
347
|
+
const body = (await ctx.req.json()) as ImportJsonlRequest;
|
|
348
|
+
const id = assertSessionId(body.entries[0]?.type === "session" ? body.entries[0].id : createSessionId());
|
|
349
|
+
const entries = body.entries[0]?.type === "session" ? body.entries : [makeHeader(id, body.cwd), ...body.entries];
|
|
350
|
+
log("import jsonl", { id, sourceName: body.sourceName, entries: entries.length, projectId: body.projectId });
|
|
351
|
+
await saveEntries(id, entries);
|
|
352
|
+
return ctx.json(snapshotFor(id, entries));
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
serve({ fetch: app.fetch, port }, (info) => {
|
|
356
|
+
console.log(`Remote session server listening on http://localhost:${info.port}`);
|
|
357
|
+
console.log(`Data directory: ${dataDir}`);
|
|
358
|
+
console.log(`Bearer token: ${token}`);
|
|
359
|
+
});
|
|
@@ -11,14 +11,14 @@ const cwd = process.cwd();
|
|
|
11
11
|
// In-memory (no persistence)
|
|
12
12
|
const inMemoryPi = await PiAgent.create({ sessionManager: new InMemorySessionManager(cwd) });
|
|
13
13
|
const inMemory = await inMemoryPi.createAgentSession();
|
|
14
|
-
console.log("In-memory session:", inMemory.
|
|
14
|
+
console.log("In-memory session:", inMemory.sessionReference ?? "(none)");
|
|
15
15
|
await inMemoryPi.dispose();
|
|
16
16
|
|
|
17
17
|
// New persistent session
|
|
18
18
|
const newSessionManager = new LocalSessionManager({ cwd });
|
|
19
19
|
const newSessionPi = await PiAgent.create({ cwd, sessionManager: newSessionManager });
|
|
20
20
|
const newSession = await newSessionPi.createAgentSession();
|
|
21
|
-
console.log("New session
|
|
21
|
+
console.log("New session reference:", newSession.sessionReference);
|
|
22
22
|
await newSessionPi.dispose();
|
|
23
23
|
|
|
24
24
|
// Continue most recent session (or create new if none)
|
|
@@ -26,7 +26,7 @@ const continuedSessionManager = new LocalSessionManager({ cwd });
|
|
|
26
26
|
const continuedPi = await PiAgent.create({ cwd, sessionManager: continuedSessionManager });
|
|
27
27
|
const continued = await continuedPi.createAgentSession({ session: continuedSessionManager.continueRecent() });
|
|
28
28
|
if (continuedPi.modelFallbackMessage) console.log("Note:", continuedPi.modelFallbackMessage);
|
|
29
|
-
console.log("Continued session:", continued.
|
|
29
|
+
console.log("Continued session:", continued.sessionReference);
|
|
30
30
|
await continuedPi.dispose();
|
|
31
31
|
|
|
32
32
|
// List and open specific session
|
|
@@ -32,17 +32,17 @@ async function bindSession() {
|
|
|
32
32
|
|
|
33
33
|
await pi.createAgentSession();
|
|
34
34
|
let session = await bindSession();
|
|
35
|
-
const
|
|
36
|
-
console.log("Initial session:",
|
|
35
|
+
const originalSessionReference = session.sessionReference;
|
|
36
|
+
console.log("Initial session:", originalSessionReference);
|
|
37
37
|
|
|
38
38
|
await pi.newSession();
|
|
39
39
|
session = await bindSession();
|
|
40
|
-
console.log("After newSession():", session.
|
|
40
|
+
console.log("After newSession():", session.sessionReference);
|
|
41
41
|
|
|
42
|
-
if (
|
|
43
|
-
await pi.switchSession(
|
|
42
|
+
if (originalSessionReference) {
|
|
43
|
+
await pi.switchSession(originalSessionReference);
|
|
44
44
|
session = await bindSession();
|
|
45
|
-
console.log("After switchSession():", session.
|
|
45
|
+
console.log("After switchSession():", session.sessionReference);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
unsubscribe?.();
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fleetagent/pi-coding-agent",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@fleetagent/pi-coding-agent",
|
|
9
|
-
"version": "0.0.
|
|
9
|
+
"version": "0.0.2",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@fleetagent/pi-agent-core": "^0.0.
|
|
13
|
-
"@fleetagent/pi-ai": "^0.0.
|
|
14
|
-
"@fleetagent/pi-tui": "^0.0.
|
|
12
|
+
"@fleetagent/pi-agent-core": "^0.0.2",
|
|
13
|
+
"@fleetagent/pi-ai": "^0.0.2",
|
|
14
|
+
"@fleetagent/pi-tui": "^0.0.2",
|
|
15
15
|
"@silvia-odwyer/photon-node": "0.3.4",
|
|
16
16
|
"chalk": "5.6.2",
|
|
17
17
|
"cross-spawn": "7.0.6",
|
|
@@ -473,11 +473,11 @@
|
|
|
473
473
|
}
|
|
474
474
|
},
|
|
475
475
|
"node_modules/@fleetagent/pi-agent-core": {
|
|
476
|
-
"version": "0.0.
|
|
477
|
-
"resolved": "https://registry.npmjs.org/@fleetagent/pi-agent-core/-/pi-agent-core-0.0.
|
|
476
|
+
"version": "0.0.2",
|
|
477
|
+
"resolved": "https://registry.npmjs.org/@fleetagent/pi-agent-core/-/pi-agent-core-0.0.2.tgz",
|
|
478
478
|
"license": "MIT",
|
|
479
479
|
"dependencies": {
|
|
480
|
-
"@fleetagent/pi-ai": "^0.0.
|
|
480
|
+
"@fleetagent/pi-ai": "^0.0.2",
|
|
481
481
|
"ignore": "7.0.5",
|
|
482
482
|
"typebox": "1.1.38",
|
|
483
483
|
"yaml": "2.9.0"
|
|
@@ -487,8 +487,8 @@
|
|
|
487
487
|
}
|
|
488
488
|
},
|
|
489
489
|
"node_modules/@fleetagent/pi-ai": {
|
|
490
|
-
"version": "0.0.
|
|
491
|
-
"resolved": "https://registry.npmjs.org/@fleetagent/pi-ai/-/pi-ai-0.0.
|
|
490
|
+
"version": "0.0.2",
|
|
491
|
+
"resolved": "https://registry.npmjs.org/@fleetagent/pi-ai/-/pi-ai-0.0.2.tgz",
|
|
492
492
|
"license": "MIT",
|
|
493
493
|
"dependencies": {
|
|
494
494
|
"@anthropic-ai/sdk": "0.91.1",
|
|
@@ -509,8 +509,8 @@
|
|
|
509
509
|
}
|
|
510
510
|
},
|
|
511
511
|
"node_modules/@fleetagent/pi-tui": {
|
|
512
|
-
"version": "0.0.
|
|
513
|
-
"resolved": "https://registry.npmjs.org/@fleetagent/pi-tui/-/pi-tui-0.0.
|
|
512
|
+
"version": "0.0.2",
|
|
513
|
+
"resolved": "https://registry.npmjs.org/@fleetagent/pi-tui/-/pi-tui-0.0.2.tgz",
|
|
514
514
|
"license": "MIT",
|
|
515
515
|
"dependencies": {
|
|
516
516
|
"get-east-asian-width": "1.6.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fleetagent/pi-coding-agent",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piConfig": {
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@fleetagent/pi-agent-core": "^0.0.
|
|
43
|
-
"@fleetagent/pi-ai": "^0.0.
|
|
44
|
-
"@fleetagent/pi-tui": "^0.0.
|
|
42
|
+
"@fleetagent/pi-agent-core": "^0.0.2",
|
|
43
|
+
"@fleetagent/pi-ai": "^0.0.2",
|
|
44
|
+
"@fleetagent/pi-tui": "^0.0.2",
|
|
45
45
|
"@silvia-odwyer/photon-node": "0.3.4",
|
|
46
46
|
"chalk": "5.6.2",
|
|
47
47
|
"cross-spawn": "7.0.6",
|