pi-webdesk 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.
Files changed (64) hide show
  1. package/README.md +111 -0
  2. package/dist/apps/daemon/src/appearance-preferences.js +218 -0
  3. package/dist/apps/daemon/src/auth.js +88 -0
  4. package/dist/apps/daemon/src/bin.js +123 -0
  5. package/dist/apps/daemon/src/cli.js +48 -0
  6. package/dist/apps/daemon/src/event-hub.js +155 -0
  7. package/dist/apps/daemon/src/index.js +102 -0
  8. package/dist/apps/daemon/src/launcher-control.js +114 -0
  9. package/dist/apps/daemon/src/launcher.js +73 -0
  10. package/dist/apps/daemon/src/pi-auth.js +290 -0
  11. package/dist/apps/daemon/src/pi-resources.js +182 -0
  12. package/dist/apps/daemon/src/pi-runtime-factory.js +19 -0
  13. package/dist/apps/daemon/src/pi-sessions.js +265 -0
  14. package/dist/apps/daemon/src/runtime-process.js +241 -0
  15. package/dist/apps/daemon/src/secret.js +71 -0
  16. package/dist/apps/daemon/src/server.js +1662 -0
  17. package/dist/apps/daemon/src/session-projection.js +117 -0
  18. package/dist/apps/daemon/src/state-lock.js +31 -0
  19. package/dist/apps/daemon/src/static-web.js +53 -0
  20. package/dist/apps/daemon/src/task-archive.js +152 -0
  21. package/dist/apps/daemon/src/task-commit.js +503 -0
  22. package/dist/apps/daemon/src/task-merge.js +912 -0
  23. package/dist/apps/daemon/src/task-review.js +204 -0
  24. package/dist/apps/daemon/src/task-runtime.js +1124 -0
  25. package/dist/apps/daemon/src/task-validation.js +352 -0
  26. package/dist/apps/daemon/src/workspace-store.js +140 -0
  27. package/dist/apps/daemon/src/workspace.js +795 -0
  28. package/dist/extensions/webdesk.js +34 -0
  29. package/dist/packages/git/src/commit.js +675 -0
  30. package/dist/packages/git/src/errors.js +55 -0
  31. package/dist/packages/git/src/fingerprint.js +286 -0
  32. package/dist/packages/git/src/index.js +123 -0
  33. package/dist/packages/git/src/merge.js +1008 -0
  34. package/dist/packages/git/src/paths.js +58 -0
  35. package/dist/packages/git/src/repository.js +77 -0
  36. package/dist/packages/git/src/review.js +396 -0
  37. package/dist/packages/git/src/runner.js +110 -0
  38. package/dist/packages/git/src/validation.js +263 -0
  39. package/dist/packages/git/src/worktree.js +233 -0
  40. package/dist/packages/pi-bridge/extensions/pita-policy.js +117 -0
  41. package/dist/packages/pi-bridge/src/auth.js +80 -0
  42. package/dist/packages/pi-bridge/src/errors.js +19 -0
  43. package/dist/packages/pi-bridge/src/handshake.js +43 -0
  44. package/dist/packages/pi-bridge/src/index.js +76 -0
  45. package/dist/packages/pi-bridge/src/jsonl.js +105 -0
  46. package/dist/packages/pi-bridge/src/policy-approval.js +62 -0
  47. package/dist/packages/pi-bridge/src/resolve.js +59 -0
  48. package/dist/packages/pi-bridge/src/resources-child.mjs +23 -0
  49. package/dist/packages/pi-bridge/src/resources.js +481 -0
  50. package/dist/packages/pi-bridge/src/rpc/client.js +480 -0
  51. package/dist/packages/pi-bridge/src/rpc/runtime.js +496 -0
  52. package/dist/packages/pi-bridge/src/rpc/supervisor.mjs +129 -0
  53. package/dist/packages/pi-bridge/src/rpc/tool-events.js +78 -0
  54. package/dist/packages/pi-bridge/src/rpc/wire.js +263 -0
  55. package/dist/packages/pi-bridge/src/runtime.js +0 -0
  56. package/dist/packages/pi-bridge/src/sessions-child.mjs +38 -0
  57. package/dist/packages/pi-bridge/src/sessions.js +314 -0
  58. package/dist/packages/pi-bridge/src/tool-activity.js +56 -0
  59. package/dist/packages/protocol/src/index.js +1863 -0
  60. package/dist/web/assets/index-BOw_fhvO.css +2 -0
  61. package/dist/web/assets/index-oXs7yAAo.js +119 -0
  62. package/dist/web/index.html +14 -0
  63. package/package.json +69 -0
  64. package/scripts/prepare.mjs +7 -0
@@ -0,0 +1,117 @@
1
+ // apps/daemon/src/session-projection.ts
2
+ import { sanitizeApprovalDisplayText } from "../../../packages/pi-bridge/src/index.js";
3
+ import {
4
+ MAX_SESSION_TRANSCRIPT_ITEMS,
5
+ MAX_SESSION_TRANSCRIPT_TEXT_CHARS
6
+ } from "../../../packages/protocol/src/index.js";
7
+ var PREVIEW_LIMIT = 160;
8
+ function clip(text, limit) {
9
+ return text.length > limit ? `${text.slice(0, limit - 1)}\u2026` : text;
10
+ }
11
+ function buildTranscript(session) {
12
+ const byId = new Map(session.entries.map((entry) => [entry.id, entry]));
13
+ const branch = [];
14
+ const visited = /* @__PURE__ */ new Set();
15
+ let cursor = session.leafId;
16
+ while (cursor !== null && !visited.has(cursor)) {
17
+ visited.add(cursor);
18
+ const entry = byId.get(cursor);
19
+ if (entry === void 0) break;
20
+ branch.push(entry);
21
+ cursor = entry.parentId;
22
+ }
23
+ branch.reverse();
24
+ const activePath = [];
25
+ const calls = /* @__PURE__ */ new Map();
26
+ for (const entry of branch) {
27
+ if ((entry.role === "user" || entry.role === "assistant") && entry.text) {
28
+ const rawPrefix = entry.text.slice(0, MAX_SESSION_TRANSCRIPT_TEXT_CHARS);
29
+ const safeText = sanitizeApprovalDisplayText(rawPrefix);
30
+ activePath.push({
31
+ id: entry.id,
32
+ role: entry.role,
33
+ text: safeText.slice(0, MAX_SESSION_TRANSCRIPT_TEXT_CHARS),
34
+ omittedChars: entry.text.length - rawPrefix.length + Math.max(0, safeText.length - MAX_SESSION_TRANSCRIPT_TEXT_CHARS)
35
+ });
36
+ }
37
+ for (const tool of entry.toolCalls ?? []) {
38
+ calls.set(tool.toolCallId, activePath.length);
39
+ activePath.push({ id: tool.toolCallId, role: "tool", tool: { ...tool, status: "incomplete" } });
40
+ }
41
+ if (entry.toolResult) {
42
+ const result = entry.toolResult;
43
+ const index = calls.get(result.toolCallId);
44
+ const existing = index === void 0 ? void 0 : activePath[index];
45
+ const tool = {
46
+ ...existing?.role === "tool" ? existing.tool : {
47
+ toolCallId: result.toolCallId,
48
+ name: result.name,
49
+ input: "",
50
+ inputOmittedChars: 0
51
+ },
52
+ status: result.isError ? "failed" : "succeeded",
53
+ output: result.output,
54
+ outputOmittedChars: result.omittedChars
55
+ };
56
+ const item = { id: result.toolCallId, role: "tool", tool };
57
+ if (index === void 0) activePath.push(item);
58
+ else activePath[index] = item;
59
+ }
60
+ }
61
+ const omittedItems = Math.max(
62
+ 0,
63
+ activePath.length - MAX_SESSION_TRANSCRIPT_ITEMS
64
+ );
65
+ return { items: activePath.slice(omittedItems), omittedItems };
66
+ }
67
+ function buildSessionTree(tree) {
68
+ const parents = /* @__PURE__ */ new Map();
69
+ const pendingParents = [...tree.roots];
70
+ while (pendingParents.length > 0) {
71
+ const node = pendingParents.pop();
72
+ parents.set(node.entry.id, node.entry.parentId);
73
+ for (const child of node.children) pendingParents.push(child);
74
+ }
75
+ const activeIds = /* @__PURE__ */ new Set();
76
+ let cursor = tree.leafId;
77
+ while (cursor !== null && !activeIds.has(cursor)) {
78
+ activeIds.add(cursor);
79
+ cursor = parents.get(cursor) ?? null;
80
+ }
81
+ const makeNode = (node) => ({
82
+ id: node.entry.id,
83
+ parentId: node.entry.parentId,
84
+ kind: sanitizeApprovalDisplayText(node.entry.type).slice(0, 100),
85
+ ...node.entry.role === void 0 ? {} : { role: sanitizeApprovalDisplayText(node.entry.role).slice(0, 100) },
86
+ ...node.label === void 0 ? {} : { label: clip(sanitizeApprovalDisplayText(node.label), 500) },
87
+ ...node.entry.text === void 0 || node.entry.text === "" ? {} : {
88
+ preview: clip(
89
+ sanitizeApprovalDisplayText(node.entry.text.slice(0, PREVIEW_LIMIT)),
90
+ PREVIEW_LIMIT
91
+ )
92
+ },
93
+ onActivePath: activeIds.has(node.entry.id),
94
+ children: []
95
+ });
96
+ const roots = tree.roots.map(makeNode);
97
+ const pendingNodes = tree.roots.map((source, index) => ({ source, target: roots[index] }));
98
+ while (pendingNodes.length > 0) {
99
+ const { source, target } = pendingNodes.pop();
100
+ target.children = source.children.map(makeNode);
101
+ for (let index = 0; index < source.children.length; index++) {
102
+ pendingNodes.push({
103
+ source: source.children[index],
104
+ target: target.children[index]
105
+ });
106
+ }
107
+ }
108
+ return {
109
+ roots,
110
+ leafId: tree.leafId,
111
+ truncated: tree.truncated
112
+ };
113
+ }
114
+ export {
115
+ buildSessionTree,
116
+ buildTranscript
117
+ };
@@ -0,0 +1,31 @@
1
+ // apps/daemon/src/state-lock.ts
2
+ import path from "node:path";
3
+ import { lock } from "proper-lockfile";
4
+ var LOCK_DIRECTORY = "daemon.lock";
5
+ var STALE_LOCK_MS = 3e4;
6
+ var LOCK_HEARTBEAT_MS = 5e3;
7
+ var DaemonStateLockError = class extends Error {
8
+ name = "DaemonStateLockError";
9
+ };
10
+ async function acquireDaemonStateLock(stateDir) {
11
+ const resolvedStateDir = path.resolve(stateDir);
12
+ try {
13
+ return await lock(resolvedStateDir, {
14
+ lockfilePath: path.join(resolvedStateDir, LOCK_DIRECTORY),
15
+ realpath: true,
16
+ stale: STALE_LOCK_MS,
17
+ update: LOCK_HEARTBEAT_MS,
18
+ retries: 0
19
+ });
20
+ } catch (error) {
21
+ const code = error !== null && typeof error === "object" && "code" in error ? String(error.code) : null;
22
+ throw new DaemonStateLockError(
23
+ code === "ELOCKED" ? `Another Webdesk daemon is already using ${resolvedStateDir}. Stop it before starting a daemon with the same state directory.` : `Webdesk could not claim exclusive ownership of ${resolvedStateDir}. Check the state directory and daemon output before retrying.`,
24
+ { cause: error }
25
+ );
26
+ }
27
+ }
28
+ export {
29
+ DaemonStateLockError,
30
+ acquireDaemonStateLock
31
+ };
@@ -0,0 +1,53 @@
1
+ // apps/daemon/src/static-web.ts
2
+ import { readFile, realpath, stat } from "node:fs/promises";
3
+ import { realpathSync } from "node:fs";
4
+ import path from "node:path";
5
+ var CONTENT_TYPES = {
6
+ ".html": "text/html; charset=utf-8",
7
+ ".js": "text/javascript; charset=utf-8",
8
+ ".css": "text/css; charset=utf-8",
9
+ ".svg": "image/svg+xml",
10
+ ".png": "image/png",
11
+ ".ico": "image/x-icon",
12
+ ".woff2": "font/woff2"
13
+ };
14
+ function createStaticWebHandler(directory) {
15
+ const root = realpathSync(directory);
16
+ return async (req, res) => {
17
+ if (req.method !== "GET" && req.method !== "HEAD") return false;
18
+ let pathname;
19
+ try {
20
+ pathname = decodeURIComponent((req.url ?? "/").split("?")[0]);
21
+ } catch {
22
+ return false;
23
+ }
24
+ if (!pathname.startsWith("/") || pathname.includes("\\") || pathname.includes("\0")) return false;
25
+ const parts = pathname.split("/");
26
+ if (parts.some((part) => part.startsWith("."))) return false;
27
+ if (pathname !== "/" && pathname !== "/pi" && pathname !== "/index.html" && !pathname.startsWith("/assets/")) return false;
28
+ const base = root;
29
+ try {
30
+ const file = await realpath(path.join(base, pathname === "/" || pathname === "/pi" ? "index.html" : pathname));
31
+ const relative = path.relative(base, file);
32
+ if (relative.startsWith("..") || path.isAbsolute(relative)) return false;
33
+ const type = CONTENT_TYPES[path.extname(file)];
34
+ if (!type || !(await stat(file)).isFile()) return false;
35
+ const body = await readFile(file);
36
+ res.writeHead(200, {
37
+ "content-type": type,
38
+ "content-length": body.length,
39
+ "cache-control": "no-store",
40
+ "referrer-policy": "no-referrer",
41
+ "x-content-type-options": "nosniff"
42
+ });
43
+ res.end(req.method === "HEAD" ? void 0 : body);
44
+ return true;
45
+ } catch (error) {
46
+ if (["ENOENT", "ENOTDIR", "EACCES"].includes(error.code ?? "")) return false;
47
+ throw error;
48
+ }
49
+ };
50
+ }
51
+ export {
52
+ createStaticWebHandler
53
+ };
@@ -0,0 +1,152 @@
1
+ // apps/daemon/src/task-archive.ts
2
+ import { realpath as resolveRealpath } from "node:fs/promises";
3
+ import { inspectRepository } from "../../../packages/git/src/index.js";
4
+ import { WorkspaceOperationError } from "./workspace.js";
5
+ import { TaskRuntimeOperationError } from "./task-runtime.js";
6
+ function createTaskArchiveService(options) {
7
+ const inspect = options.inspectRepository ?? ((rootPath) => inspectRepository(rootPath));
8
+ const realpath = options.realpath ?? resolveRealpath;
9
+ const active = /* @__PURE__ */ new Set();
10
+ function claim(taskId) {
11
+ if (active.has(taskId)) {
12
+ throw new WorkspaceOperationError(
13
+ "task-busy",
14
+ "Another archive or restore operation is already active for this task.",
15
+ 409
16
+ );
17
+ }
18
+ active.add(taskId);
19
+ }
20
+ function assertOtherLifecycleIdle(taskId, repositoryId) {
21
+ if (options.isRuntimeMutating(taskId) || options.isValidationRunning(taskId) || options.isCommitActive(taskId) || options.isMergeActive(taskId) || repositoryId !== void 0 && options.isMergeRepositoryActive(repositoryId)) {
22
+ throw new WorkspaceOperationError(
23
+ "task-busy",
24
+ "Wait for the active Pi, validation, commit, or merge operation to settle before changing this task's lifecycle.",
25
+ 409
26
+ );
27
+ }
28
+ }
29
+ async function requireTask(taskId) {
30
+ const task = await options.getTask(taskId);
31
+ if (task === null) {
32
+ throw new WorkspaceOperationError(
33
+ "task-not-found",
34
+ "The selected task no longer exists.",
35
+ 404
36
+ );
37
+ }
38
+ return task;
39
+ }
40
+ return {
41
+ isBusy(taskId) {
42
+ return active.has(taskId);
43
+ },
44
+ async archive(taskId) {
45
+ claim(taskId);
46
+ try {
47
+ assertOtherLifecycleIdle(taskId);
48
+ const task = await requireTask(taskId);
49
+ assertOtherLifecycleIdle(taskId, task.repositoryId);
50
+ if (task.status === "archived") return options.archiveTask(taskId);
51
+ if (task.status !== "ready") {
52
+ throw new WorkspaceOperationError(
53
+ "task-not-ready",
54
+ `Only a ready task can be archived; this task is ${task.status}.`,
55
+ 409
56
+ );
57
+ }
58
+ const repository = await options.getRepository(task.repositoryId);
59
+ if (repository === null) {
60
+ throw new WorkspaceOperationError(
61
+ "task-recovery-required",
62
+ "The task's registered repository is unavailable, so it was not archived.",
63
+ 409
64
+ );
65
+ }
66
+ if (repository.pendingMerge?.taskId === task.id) {
67
+ throw new WorkspaceOperationError(
68
+ "task-busy",
69
+ "Reconcile the task's interrupted merge before archiving it.",
70
+ 409
71
+ );
72
+ }
73
+ try {
74
+ await options.disposeRuntime(taskId);
75
+ } catch (error) {
76
+ if (error instanceof TaskRuntimeOperationError) {
77
+ throw new WorkspaceOperationError(
78
+ error.code === "runtime-busy" ? "task-busy" : "task-recovery-required",
79
+ error.message,
80
+ error.status,
81
+ { cause: error }
82
+ );
83
+ }
84
+ throw error;
85
+ }
86
+ const stopped = await requireTask(taskId);
87
+ if (stopped.status !== "ready" || stopped.runtimeProcess !== void 0) {
88
+ throw new WorkspaceOperationError(
89
+ "task-recovery-required",
90
+ "Webdesk could not prove that the task runtime stopped cleanly, so the task was not archived.",
91
+ 409
92
+ );
93
+ }
94
+ options.invalidateCommitPreflight(taskId);
95
+ options.invalidateMergePreflight(taskId);
96
+ return await options.archiveTask(taskId);
97
+ } finally {
98
+ active.delete(taskId);
99
+ }
100
+ },
101
+ async restore(taskId) {
102
+ claim(taskId);
103
+ try {
104
+ assertOtherLifecycleIdle(taskId);
105
+ const task = await requireTask(taskId);
106
+ assertOtherLifecycleIdle(taskId, task.repositoryId);
107
+ if (task.status === "ready") return options.restoreTask(taskId, 0);
108
+ if (task.status !== "archived") {
109
+ throw new WorkspaceOperationError(
110
+ "task-not-ready",
111
+ `Only an archived task can be restored; this task is ${task.status}.`,
112
+ 409
113
+ );
114
+ }
115
+ const repository = await options.getRepository(task.repositoryId);
116
+ if (repository === null) {
117
+ throw new WorkspaceOperationError(
118
+ "task-recovery-required",
119
+ "The task's registered repository is unavailable, so it remains archived.",
120
+ 409
121
+ );
122
+ }
123
+ let canonicalRoot;
124
+ let worktree;
125
+ try {
126
+ canonicalRoot = await realpath(task.worktreePath);
127
+ worktree = await inspect(canonicalRoot);
128
+ } catch (error) {
129
+ throw new WorkspaceOperationError(
130
+ "task-recovery-required",
131
+ "The archived task worktree is unavailable or no longer a valid Git worktree.",
132
+ 409,
133
+ { cause: error }
134
+ );
135
+ }
136
+ if (canonicalRoot !== task.worktreePath || worktree.root !== canonicalRoot || worktree.commonDir !== repository.commonDir || worktree.branch !== task.branch) {
137
+ throw new WorkspaceOperationError(
138
+ "task-recovery-required",
139
+ "The archived worktree no longer matches its recorded path, repository, or branch, so it was not restored.",
140
+ 409
141
+ );
142
+ }
143
+ return await options.restoreTask(taskId, task.archivedAtMs);
144
+ } finally {
145
+ active.delete(taskId);
146
+ }
147
+ }
148
+ };
149
+ }
150
+ export {
151
+ createTaskArchiveService
152
+ };