@timqi/pier 0.0.17 → 0.0.20

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.
Files changed (95) hide show
  1. package/dist/agent/config-sync.js +123 -0
  2. package/dist/agent/config.js +95 -3
  3. package/dist/agent/events.js +26 -5
  4. package/dist/agent/listing.js +12 -13
  5. package/dist/agent/pi.js +75 -27
  6. package/dist/boards/boards.js +7 -6
  7. package/dist/channels/attach.js +18 -4
  8. package/dist/channels/config.js +7 -2
  9. package/dist/channels/dedup.js +9 -1
  10. package/dist/channels/lark.js +2 -6
  11. package/dist/channels/receipts.js +21 -6
  12. package/dist/channels/slack-tool.js +86 -25
  13. package/dist/channels/slack.js +2 -10
  14. package/dist/channels/telegram.js +1 -0
  15. package/dist/config-sync-fetch.js +97 -0
  16. package/dist/config-sync-task.js +59 -0
  17. package/dist/config-sync.js +179 -0
  18. package/dist/core/hub.js +23 -6
  19. package/dist/core/inbox.js +1 -1
  20. package/dist/core/reply.js +76 -2
  21. package/dist/core/router.js +162 -37
  22. package/dist/db.js +50 -0
  23. package/dist/main.js +32 -1
  24. package/dist/secrets.js +18 -6
  25. package/dist/tasks/agent.js +25 -23
  26. package/dist/tasks/callbacks.js +1 -1
  27. package/dist/tasks/definitions.js +28 -17
  28. package/dist/tasks/execution.js +11 -3
  29. package/dist/tasks/messages.js +58 -16
  30. package/dist/tasks/routes.js +58 -10
  31. package/dist/tasks/runs.js +0 -5
  32. package/dist/tasks/service.js +14 -4
  33. package/dist/tasks/store.js +87 -10
  34. package/dist/tasks/tool.js +8 -4
  35. package/dist/tools.js +32 -2
  36. package/dist/web/auth.js +17 -23
  37. package/dist/web/config-sync.js +91 -0
  38. package/dist/web/config.js +61 -0
  39. package/dist/web/explorer.js +20 -70
  40. package/dist/web/fs.js +159 -0
  41. package/dist/web/instance.js +11 -0
  42. package/dist/web/public/assets/activity-DYo7QS7k.js +5 -0
  43. package/dist/web/public/assets/activity-DYo7QS7k.js.br +0 -0
  44. package/dist/web/public/assets/activity-DYo7QS7k.js.gz +0 -0
  45. package/dist/web/public/assets/boards-CwohTbtv.js +1 -0
  46. package/dist/web/public/assets/boards-CwohTbtv.js.br +0 -0
  47. package/dist/web/public/assets/boards-CwohTbtv.js.gz +0 -0
  48. package/dist/web/public/assets/code-p-Tq1bMK.js +1 -0
  49. package/dist/web/public/assets/explorer-50no_DrD.js +5 -0
  50. package/dist/web/public/assets/explorer-50no_DrD.js.br +0 -0
  51. package/dist/web/public/assets/explorer-50no_DrD.js.gz +0 -0
  52. package/dist/web/public/assets/{ghostty-web-CV7F9_iW.js → ghostty-web-Bvhh3ZFk.js} +1 -1
  53. package/dist/web/public/assets/ghostty-web-Bvhh3ZFk.js.br +0 -0
  54. package/dist/web/public/assets/ghostty-web-Bvhh3ZFk.js.gz +0 -0
  55. package/dist/web/public/assets/hljs-tWqyD59G.js +3 -0
  56. package/dist/web/public/assets/hljs-tWqyD59G.js.br +0 -0
  57. package/dist/web/public/assets/hljs-tWqyD59G.js.gz +0 -0
  58. package/dist/web/public/assets/{index-CtR-Ob4T.css → index-DuZ2vERd.css} +1 -1
  59. package/dist/web/public/assets/index-DuZ2vERd.css.br +0 -0
  60. package/dist/web/public/assets/index-DuZ2vERd.css.gz +0 -0
  61. package/dist/web/public/assets/index-WrsNstrb.js +79 -0
  62. package/dist/web/public/assets/index-WrsNstrb.js.br +0 -0
  63. package/dist/web/public/assets/index-WrsNstrb.js.gz +0 -0
  64. package/dist/web/public/assets/runs-BAXZF-6r.js +1 -0
  65. package/dist/web/public/assets/runs-BAXZF-6r.js.br +0 -0
  66. package/dist/web/public/assets/runs-BAXZF-6r.js.gz +0 -0
  67. package/dist/web/public/assets/settings-BmNXgRne.js +6 -0
  68. package/dist/web/public/assets/settings-BmNXgRne.js.br +0 -0
  69. package/dist/web/public/assets/settings-BmNXgRne.js.gz +0 -0
  70. package/dist/web/public/assets/task-runs-CpgHxFLf.js +3 -0
  71. package/dist/web/public/assets/task-runs-CpgHxFLf.js.br +0 -0
  72. package/dist/web/public/assets/task-runs-CpgHxFLf.js.gz +0 -0
  73. package/dist/web/public/assets/tasks-DybtogPF.js +4 -0
  74. package/dist/web/public/assets/tasks-DybtogPF.js.br +0 -0
  75. package/dist/web/public/assets/tasks-DybtogPF.js.gz +0 -0
  76. package/dist/web/public/assets/terminal-m2_WYu5W.js +2 -0
  77. package/dist/web/public/assets/terminal-m2_WYu5W.js.br +0 -0
  78. package/dist/web/public/assets/terminal-m2_WYu5W.js.gz +0 -0
  79. package/dist/web/public/icon.svg.br +0 -0
  80. package/dist/web/public/icon.svg.gz +0 -0
  81. package/dist/web/public/index.html +13 -2
  82. package/dist/web/public/index.html.br +0 -0
  83. package/dist/web/public/index.html.gz +0 -0
  84. package/dist/web/public/manifest.webmanifest.br +0 -0
  85. package/dist/web/public/manifest.webmanifest.gz +0 -0
  86. package/dist/web/public/sw.js.br +0 -0
  87. package/dist/web/public/sw.js.gz +0 -0
  88. package/dist/web/route.js +17 -0
  89. package/dist/web/server.js +152 -66
  90. package/dist/web/terminal.js +2 -7
  91. package/package.json +2 -2
  92. package/skills/pier-slack/SKILL.md +4 -16
  93. package/skills/pier-tasks/SKILL.md +14 -9
  94. package/dist/web/files.js +0 -183
  95. package/dist/web/public/assets/index-DAxKtidH.js +0 -93
package/dist/web/files.js DELETED
@@ -1,183 +0,0 @@
1
- // Filesystem-facing routes: scoped Pi config editing, directory browsing for
2
- // the cwd pickers, and session attachments. Split from server.ts, which keeps
3
- // the session/queue/SSE routes; `guarded` lives here because both halves wrap
4
- // their route bodies with it.
5
- import { mkdir, readdir, readFile, realpath, stat } from "node:fs/promises";
6
- import { homedir } from "node:os";
7
- import { basename, dirname, extname, isAbsolute, resolve, sep } from "node:path";
8
- import { INBOX_DIR } from "../core/inbox.js";
9
- // Content types for the attachment route. Anything unlisted downloads as
10
- // bytes — guessing a type we can't vouch for is how a file starts executing.
11
- const FILE_TYPES = {
12
- ".png": "image/png",
13
- ".jpg": "image/jpeg",
14
- ".jpeg": "image/jpeg",
15
- ".gif": "image/gif",
16
- ".webp": "image/webp",
17
- ".avif": "image/avif",
18
- ".bmp": "image/bmp",
19
- ".pdf": "application/pdf",
20
- ".txt": "text/plain; charset=utf-8",
21
- ".md": "text/plain; charset=utf-8",
22
- ".log": "text/plain; charset=utf-8",
23
- ".json": "text/plain; charset=utf-8",
24
- ".csv": "text/plain; charset=utf-8",
25
- ".yaml": "text/plain; charset=utf-8",
26
- ".yml": "text/plain; charset=utf-8",
27
- };
28
- const MAX_FILE_BYTES = 32 * 1024 * 1024;
29
- /** Register a route whose body may throw: the error becomes the JSON shape
30
- * the UI expects, with the status the route owns. Takes the literal path so
31
- * `c.req.param()` keeps its typed keys. */
32
- export function guarded(app, method, path, status, fn) {
33
- app.on(method, path, async (c) => {
34
- try {
35
- return await fn(c);
36
- }
37
- catch (err) {
38
- return c.json({ error: String(err) }, status);
39
- }
40
- });
41
- }
42
- export function registerFileRoutes(app, { factory, config, nascentCwd, onConfigWritten }) {
43
- // Scope comes from the client as "global" or a project cwd; only cwds Pi
44
- // already knows (the session list) are accepted — never an arbitrary path.
45
- const parseScope = async (raw) => {
46
- if (!raw || raw === "global")
47
- return { kind: "global" };
48
- const known = await factory.list();
49
- return known.some((s) => s.cwd === raw) ? { kind: "project", cwd: raw } : null;
50
- };
51
- app.get("/api/config", async (c) => {
52
- c.header("cache-control", "no-store");
53
- const scope = await parseScope(c.req.query("scope"));
54
- if (!scope)
55
- return c.json({ error: "unknown scope" }, 400);
56
- return c.json({
57
- // Where this scope's files live on disk — the UI labels "Global" with it.
58
- dir: scope.kind === "global" ? config.globalDir : scope.cwd,
59
- files: await config.listFiles(scope),
60
- resources: await config.listResources(scope),
61
- });
62
- });
63
- guarded(app, "GET", "/api/config/files/:name", 400, async (c) => {
64
- c.header("cache-control", "no-store");
65
- const scope = await parseScope(c.req.query("scope"));
66
- if (!scope)
67
- return c.json({ error: "unknown scope" }, 400);
68
- return c.json({ content: await config.readFile(scope, c.req.param("name")) });
69
- });
70
- guarded(app, "PUT", "/api/config/files/:name", 400, async (c) => {
71
- const scope = await parseScope(c.req.query("scope"));
72
- if (!scope)
73
- return c.json({ error: "unknown scope" }, 400);
74
- const body = await c.req.json().catch(() => null);
75
- if (typeof body?.content !== "string" || typeof body?.expected !== "string") {
76
- return c.json({ error: "content and expected content required" }, 400);
77
- }
78
- const name = c.req.param("name");
79
- await config.writeFile(scope, name, body.content, body.expected);
80
- onConfigWritten?.();
81
- return c.json({ ok: true, content: await config.readFile(scope, name) });
82
- });
83
- // Resource names may contain slashes — query params, not path params.
84
- guarded(app, "GET", "/api/config/resource", 400, async (c) => {
85
- c.header("cache-control", "no-store");
86
- const scope = await parseScope(c.req.query("scope"));
87
- if (!scope)
88
- return c.json({ error: "unknown scope" }, 400);
89
- const kind = c.req.query("kind");
90
- const name = c.req.query("name");
91
- if ((kind !== "extensions" && kind !== "skills") || !name) {
92
- return c.json({ error: "kind and name required" }, 400);
93
- }
94
- return c.json({ content: await config.readResource(scope, kind, name) });
95
- });
96
- /** Real path of a file a session may expose: inside its cwd or the inbox
97
- * (where inbound user attachments land — core/inbox.ts), nothing else. */
98
- const resolveFile = async (id, raw) => {
99
- const cwd = nascentCwd(id) ?? (await factory.find(id))?.cwd;
100
- if (!cwd || !isAbsolute(raw))
101
- return null;
102
- try {
103
- // realpath both ends, so neither `..` nor a symlink can step outside.
104
- const roots = [await realpath(cwd)];
105
- try {
106
- roots.push(await realpath(INBOX_DIR));
107
- }
108
- catch {
109
- /* no inbox yet — nothing was ever uploaded */
110
- }
111
- const target = await realpath(resolve(roots[0], raw));
112
- if (!roots.some((root) => target === root || target.startsWith(root + sep)))
113
- return null;
114
- return (await stat(target)).isFile() ? target : null;
115
- }
116
- catch {
117
- return null; // missing, unreadable, or not a file
118
- }
119
- };
120
- // Attachments, both directions: the agent links a file it produced, the
121
- // chat renders a file the user sent — the client fetches the bytes here,
122
- // read-only, and only from the session's own cwd or the inbox.
123
- app.get("/api/sessions/:id/files", async (c) => {
124
- const raw = c.req.query("path");
125
- if (!raw)
126
- return c.json({ error: "path required" }, 400);
127
- const file = await resolveFile(c.req.param("id"), raw);
128
- if (!file)
129
- return c.json({ error: "no such file" }, 404);
130
- const { size } = await stat(file);
131
- if (size > MAX_FILE_BYTES)
132
- return c.json({ error: "file too large" }, 413);
133
- const name = basename(file);
134
- // Unknown extension → octet-stream: never let the browser guess a type we
135
- // can't vouch for (that is how an attachment starts executing).
136
- const type = FILE_TYPES[extname(file).toLowerCase()] ?? "application/octet-stream";
137
- const disposition = c.req.query("download") === "1" ? "attachment" : "inline";
138
- return c.body(await readFile(file), 200, {
139
- "content-type": type,
140
- "content-disposition": `${disposition}; filename*=UTF-8''${encodeURIComponent(name)}`,
141
- "cache-control": "private, max-age=60",
142
- });
143
- });
144
- // Directory browsing for the working-directory picker (new session, IM chat
145
- // config). Names only, never contents — the file route above stays the only
146
- // way to read bytes, and it is scoped to a session's own cwd.
147
- app.get("/api/fs/dirs", async (c) => {
148
- const raw = c.req.query("path");
149
- const path = raw ? resolve(raw) : homedir();
150
- if (!isAbsolute(path))
151
- return c.json({ error: "absolute path required" }, 400);
152
- let entries = [];
153
- try {
154
- const dir = await readdir(path, { withFileTypes: true });
155
- entries = dir
156
- // Dotfiles are noise in a project picker; a hidden cwd can still be
157
- // typed by hand.
158
- .filter((e) => e.isDirectory() && !e.name.startsWith("."))
159
- .map((e) => e.name)
160
- .sort((a, b) => a.localeCompare(b));
161
- }
162
- catch {
163
- return c.json({ error: "cannot read directory" }, 404);
164
- }
165
- const parent = dirname(path);
166
- return c.json({ path, parent: parent === path ? null : parent, entries });
167
- });
168
- // Create a folder while picking one — a new project usually needs a new
169
- // directory. A name, never a path: traversal is rejected, not normalized.
170
- guarded(app, "POST", "/api/fs/dirs", 400, async (c) => {
171
- const body = await c.req.json().catch(() => null);
172
- const parent = typeof body?.path === "string" ? body.path : "";
173
- const name = typeof body?.name === "string" ? body.name.trim() : "";
174
- if (!isAbsolute(parent))
175
- return c.json({ error: "absolute path required" }, 400);
176
- if (!name || name.length > 64 || /[/\\]|^\.\.?$/.test(name)) {
177
- return c.json({ error: "invalid folder name" }, 400);
178
- }
179
- const path = resolve(parent, name);
180
- await mkdir(path); // no recursive: the parent must already exist
181
- return c.json({ path });
182
- });
183
- }