@timqi/pier 0.0.8 → 0.0.9
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 +26 -9
- package/dist/agent/pi.js +103 -5
- package/dist/channels/conversations.js +10 -0
- package/dist/core/router.js +27 -11
- package/dist/db.js +9 -0
- package/dist/extensions/index.js +34 -0
- package/dist/extensions/web/anthropic.js +118 -0
- package/dist/extensions/web/artifacts.js +57 -0
- package/dist/extensions/web/content.js +130 -0
- package/dist/extensions/web/http.js +106 -0
- package/dist/extensions/web/index.js +9 -0
- package/dist/extensions/web/json.js +5 -0
- package/dist/extensions/web/language.js +47 -0
- package/dist/extensions/web/openai.js +112 -0
- package/dist/extensions/web/provider.js +121 -0
- package/dist/extensions/web/tools.js +284 -0
- package/dist/main.js +30 -5
- package/dist/paths.js +15 -0
- package/dist/settings.js +68 -13
- package/dist/web/instance.js +32 -8
- package/dist/web/providers.js +16 -0
- package/dist/web/public/assets/{ghostty-web-CcIc8O2I.js → ghostty-web-C4N9kjtH.js} +1 -1
- package/dist/web/public/assets/index-DNCJJRSS.js +91 -0
- package/dist/web/public/assets/{index-gcSJ9QZ5.css → index-DYl1xk5y.css} +1 -1
- package/dist/web/public/index.html +2 -2
- package/dist/web/push.js +11 -2
- package/dist/web/server.js +41 -4
- package/dist/web/session-state.js +40 -9
- package/dist/web/terminal.js +34 -4
- package/package.json +1 -1
- package/dist/web/public/assets/index-DmDJKOLH.js +0 -90
|
@@ -22,23 +22,42 @@ export class SessionStateStore {
|
|
|
22
22
|
}
|
|
23
23
|
/** Pin plus the summary Projects needs, atomically in one row. */
|
|
24
24
|
pin(summary, pinned) {
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// A new session joins a project that already has a place in the list.
|
|
26
|
+
// Unranked it would sort on top — lifting the whole project with it, which
|
|
27
|
+
// is the jump manual order exists to stop.
|
|
28
|
+
const sibling = this.#db.prepare("SELECT project_sort AS rank FROM session_state WHERE cwd = ? AND project_sort IS NOT NULL LIMIT 1").get(summary.cwd);
|
|
29
|
+
this.#db.prepare(`INSERT INTO session_state(session_id, pinned, cwd, title, created_at, project_sort)
|
|
30
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
27
31
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
28
32
|
pinned = excluded.pinned,
|
|
29
33
|
cwd = excluded.cwd,
|
|
30
34
|
title = COALESCE(excluded.title, session_state.title),
|
|
31
|
-
created_at = excluded.created_at
|
|
35
|
+
created_at = excluded.created_at,
|
|
36
|
+
project_sort = COALESCE(session_state.project_sort, excluded.project_sort)`).run(summary.id, pinned ? 1 : 0, summary.cwd, summary.title ?? null, summary.createdAt, sibling?.rank ?? null);
|
|
37
|
+
}
|
|
38
|
+
/** One drag = one write of the whole list it reordered: index is the place.
|
|
39
|
+
* `sessions` are ids (a session's place inside its project), `projects` are
|
|
40
|
+
* cwds, whose place is stamped on every session that has that cwd. */
|
|
41
|
+
reorder(order) {
|
|
42
|
+
const bySession = this.#db.prepare("UPDATE session_state SET sort = ? WHERE session_id = ?");
|
|
43
|
+
const byCwd = this.#db.prepare("UPDATE session_state SET project_sort = ? WHERE cwd = ?");
|
|
44
|
+
this.#tx(() => {
|
|
45
|
+
order.sessions?.forEach((id, i) => bySession.run(i, id));
|
|
46
|
+
order.projects?.forEach((cwd, i) => byCwd.run(i, cwd));
|
|
47
|
+
});
|
|
32
48
|
}
|
|
33
49
|
/** Project rows only; unlike AgentFactory.list(), this never touches disk. */
|
|
34
50
|
projects() {
|
|
35
|
-
const rows = this.#db.prepare(`SELECT session_id AS id, cwd, title, created_at AS createdAt, unread
|
|
51
|
+
const rows = this.#db.prepare(`SELECT session_id AS id, cwd, title, created_at AS createdAt, unread,
|
|
52
|
+
sort, project_sort AS projectSort
|
|
36
53
|
FROM session_state
|
|
37
54
|
WHERE pinned = 1 AND cwd IS NOT NULL AND created_at IS NOT NULL
|
|
38
55
|
ORDER BY created_at DESC`).all();
|
|
39
|
-
return rows.map(({ title, unread, ...row }) => ({
|
|
56
|
+
return rows.map(({ title, unread, sort, projectSort, ...row }) => ({
|
|
40
57
|
...row,
|
|
41
58
|
...(title ? { title } : {}),
|
|
59
|
+
...(sort === null ? {} : { sort }),
|
|
60
|
+
...(projectSort === null ? {} : { projectSort }),
|
|
42
61
|
unread: unread === 1,
|
|
43
62
|
}));
|
|
44
63
|
}
|
|
@@ -58,10 +77,16 @@ export class SessionStateStore {
|
|
|
58
77
|
const update = this.#db.prepare(`UPDATE session_state SET
|
|
59
78
|
cwd = ?, title = COALESCE(?, title), created_at = ?
|
|
60
79
|
WHERE session_id = ?`);
|
|
61
|
-
this.#
|
|
62
|
-
try {
|
|
80
|
+
this.#tx(() => {
|
|
63
81
|
for (const s of summaries)
|
|
64
82
|
update.run(s.cwd, s.title ?? null, s.createdAt, s.id);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** All-or-nothing: a half-written order is a list nobody arranged. */
|
|
86
|
+
#tx(run) {
|
|
87
|
+
this.#db.exec("BEGIN");
|
|
88
|
+
try {
|
|
89
|
+
run();
|
|
65
90
|
this.#db.exec("COMMIT");
|
|
66
91
|
}
|
|
67
92
|
catch (err) {
|
|
@@ -70,8 +95,14 @@ export class SessionStateStore {
|
|
|
70
95
|
}
|
|
71
96
|
}
|
|
72
97
|
flags() {
|
|
73
|
-
const rows = this.#db.prepare(
|
|
74
|
-
|
|
98
|
+
const rows = this.#db.prepare(`SELECT session_id AS id, pinned, unread, sort, project_sort AS projectSort
|
|
99
|
+
FROM session_state WHERE pinned = 1 OR unread = 1`).all();
|
|
100
|
+
return new Map(rows.map((r) => [r.id, {
|
|
101
|
+
pinned: r.pinned === 1,
|
|
102
|
+
unread: r.unread === 1,
|
|
103
|
+
...(r.sort === null ? {} : { sort: r.sort }),
|
|
104
|
+
...(r.projectSort === null ? {} : { projectSort: r.projectSort }),
|
|
105
|
+
}]));
|
|
75
106
|
}
|
|
76
107
|
/** The first prompt supplies the title of a newly-created pinned session. */
|
|
77
108
|
title(sessionId, text) {
|
package/dist/web/terminal.js
CHANGED
|
@@ -28,6 +28,15 @@ const MAX_FRAME_BYTES = 1024 * 1024;
|
|
|
28
28
|
// start under. Inheriting these can attach the shell back into Pier's parent
|
|
29
29
|
// tmux session; SSH_AUTH_SOCK deliberately stays so git/ssh keep working.
|
|
30
30
|
const PARENT_TERMINAL_ENV = ["TMUX", "TMUX_PANE", "SSH_TTY", "SSH_CLIENT", "SSH_CONNECTION"];
|
|
31
|
+
// Pier's own configuration is Pier's, not this shell's. `NODE_ENV=production`
|
|
32
|
+
// alone turns an `npm i` typed here into an install with no dev dependencies,
|
|
33
|
+
// and every `PI_*`/`PIER_*` would point a `pi` started here at Pier's own
|
|
34
|
+
// instance rather than the person's. Everything else is inherited on purpose:
|
|
35
|
+
// PATH, LANG, SSH_AUTH_SOCK and the session's XDG/DBUS handles are what make
|
|
36
|
+
// the shell usable, and on a service-managed instance nothing else supplies
|
|
37
|
+
// them.
|
|
38
|
+
const PIER_OWN_ENV = ["NODE_ENV", "PORT", "HOST"];
|
|
39
|
+
const PIER_OWN_PREFIX = /^PI(ER)?_/;
|
|
31
40
|
const send = (sock, data) => {
|
|
32
41
|
try {
|
|
33
42
|
sock.send(data);
|
|
@@ -44,6 +53,7 @@ export class TerminalHub {
|
|
|
44
53
|
#shell;
|
|
45
54
|
#idleMs;
|
|
46
55
|
#maxTerms;
|
|
56
|
+
#initCommand;
|
|
47
57
|
#sweeper;
|
|
48
58
|
#closeListeners = new Set();
|
|
49
59
|
#closed = false;
|
|
@@ -51,6 +61,7 @@ export class TerminalHub {
|
|
|
51
61
|
this.#shell = opts.shell ?? process.env.SHELL ?? "/bin/bash";
|
|
52
62
|
this.#idleMs = opts.idleMs ?? IDLE_MS;
|
|
53
63
|
this.#maxTerms = opts.maxTerms ?? MAX_TERMS;
|
|
64
|
+
this.#initCommand = opts.initCommand ?? (() => "");
|
|
54
65
|
this.#sweeper = setInterval(() => this.sweep(Date.now()), SWEEP_MS);
|
|
55
66
|
this.#sweeper.unref();
|
|
56
67
|
}
|
|
@@ -117,8 +128,11 @@ export class TerminalHub {
|
|
|
117
128
|
}
|
|
118
129
|
#spawn(cwd) {
|
|
119
130
|
const env = { ...process.env };
|
|
120
|
-
for (const key of
|
|
121
|
-
|
|
131
|
+
for (const key of Object.keys(env)) {
|
|
132
|
+
if (PARENT_TERMINAL_ENV.includes(key) || PIER_OWN_ENV.includes(key) || PIER_OWN_PREFIX.test(key)) {
|
|
133
|
+
delete env[key];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
122
136
|
const pty = spawn(this.#shell, [], {
|
|
123
137
|
name: "xterm-256color",
|
|
124
138
|
cols: 120,
|
|
@@ -129,6 +143,14 @@ export class TerminalHub {
|
|
|
129
143
|
const term = { pty, ring: [], ringBytes: 0, clients: new Set(), idleSince: Infinity };
|
|
130
144
|
this.#terms.set(cwd, term);
|
|
131
145
|
log.info(`shell ${pty.pid} for ${cwd}`);
|
|
146
|
+
// Typed in, not exec'd: the shell stays the parent, so quitting whatever
|
|
147
|
+
// this starts leaves a usable prompt, and the echo plus any error is in
|
|
148
|
+
// the ring where the person can see what ran. The tty buffers it until the
|
|
149
|
+
// shell's first read, so no wait is needed. A reattach never repeats it —
|
|
150
|
+
// this runs once per pty, which is once per cwd.
|
|
151
|
+
const init = this.#initCommand().trim();
|
|
152
|
+
if (init)
|
|
153
|
+
pty.write(`${init}\r`);
|
|
132
154
|
pty.onData((data) => {
|
|
133
155
|
const chunk = Buffer.from(data);
|
|
134
156
|
term.ring.push(chunk);
|
|
@@ -179,6 +201,13 @@ export class TerminalHub {
|
|
|
179
201
|
term.pty.write(msg.d);
|
|
180
202
|
return;
|
|
181
203
|
}
|
|
204
|
+
// The only way a page can end a shell: everything attached to it is told by
|
|
205
|
+
// the exit path below, exactly as if the shell had exited on its own.
|
|
206
|
+
if (msg.t === "restart") {
|
|
207
|
+
log.info(`shell ${term.pty.pid} for ${cwd} killed on request`);
|
|
208
|
+
term.pty.kill();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
182
211
|
if (msg.t === "resize" &&
|
|
183
212
|
typeof msg.cols === "number" && typeof msg.rows === "number" &&
|
|
184
213
|
Number.isInteger(msg.cols) && Number.isInteger(msg.rows) &&
|
|
@@ -225,8 +254,9 @@ export class TerminalHub {
|
|
|
225
254
|
/** The upgrade seam: `/api/terminal?cwd=…` behind the same password boundary
|
|
226
255
|
* as every route. SameSite=Lax already withholds the cookie cross-site; the
|
|
227
256
|
* Origin check is the explicit copy of that fact. */
|
|
228
|
-
export function attachTerminal(server, auth,
|
|
229
|
-
const
|
|
257
|
+
export function attachTerminal(server, auth, opts = {}) {
|
|
258
|
+
const heartbeatMs = opts.heartbeatMs ?? HEARTBEAT_MS;
|
|
259
|
+
const hub = new TerminalHub(opts);
|
|
230
260
|
const wss = new WebSocketServer({ noServer: true, maxPayload: MAX_FRAME_BYTES });
|
|
231
261
|
const alive = new WeakSet();
|
|
232
262
|
const heartbeat = setInterval(() => {
|
package/package.json
CHANGED