pi2web 0.2.1 → 0.2.8

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 (54) hide show
  1. package/README.md +53 -6
  2. package/dist/activity.js +178 -0
  3. package/dist/agent-feed.js +133 -0
  4. package/dist/agent-templates.js +144 -0
  5. package/dist/agents.js +671 -152
  6. package/dist/auth.js +3 -1
  7. package/dist/collab/baseline.js +84 -0
  8. package/dist/collab/dispatcher.js +45 -10
  9. package/dist/collab/hub.js +1392 -384
  10. package/dist/collab/pi-extension.js +322 -65
  11. package/dist/collab/review-flow.js +197 -48
  12. package/dist/collab/routes.js +94 -23
  13. package/dist/collab/schemas.js +69 -13
  14. package/dist/collab/scoring-flow.js +145 -39
  15. package/dist/collab/similarity.js +15 -0
  16. package/dist/collab/store.js +467 -112
  17. package/dist/collab/types.js +94 -10
  18. package/dist/collab/validate.js +62 -25
  19. package/dist/drafts.js +26 -6
  20. package/dist/http/agent-routes.js +222 -0
  21. package/dist/http/login-limiter.js +53 -0
  22. package/dist/http/mail-settings-routes.js +53 -0
  23. package/dist/http/system-prompt-routes.js +52 -0
  24. package/dist/http/utils.js +47 -0
  25. package/dist/http/web-assets.js +64 -0
  26. package/dist/http/workspace-routes.js +58 -0
  27. package/dist/mail-notifier.js +63 -17
  28. package/dist/main.js +61 -16
  29. package/dist/mcp.js +24 -5
  30. package/dist/metadata-store.js +203 -56
  31. package/dist/paths.js +10 -0
  32. package/dist/sdk-backend.js +249 -45
  33. package/dist/server.js +488 -614
  34. package/dist/session-namer.js +66 -17
  35. package/dist/session-performance.js +137 -0
  36. package/dist/sotto-stt.js +217 -0
  37. package/dist/terminals.js +94 -27
  38. package/dist/timeline.js +185 -0
  39. package/dist/voice.js +153 -35
  40. package/dist/workspaces.js +91 -26
  41. package/dist/ws/agent-socket.js +256 -0
  42. package/dist/ws/terminal-socket.js +46 -0
  43. package/npm-shrinkwrap.json +3685 -0
  44. package/package.json +11 -1
  45. package/packages/protocol/openapi.yaml +24 -2
  46. package/packages/protocol/websocket.schema.json +58 -15
  47. package/web/activity-view.js +121 -0
  48. package/web/app.js +3647 -756
  49. package/web/backend-state.js +59 -27
  50. package/web/collab.js +684 -257
  51. package/web/hid-keyboard.js +173 -140
  52. package/web/index.html +7 -2
  53. package/web/styles.css +21 -2
  54. package/web/voice-pcm.js +36 -0
package/dist/auth.js CHANGED
@@ -58,7 +58,9 @@ export class AuthStore {
58
58
  if (same(data.voiceTokenHash))
59
59
  return 'voice';
60
60
  }
61
- catch { /* absent credential */ }
61
+ catch {
62
+ /* absent credential */
63
+ }
62
64
  return undefined;
63
65
  }
64
66
  async rotateVoice() {
@@ -0,0 +1,84 @@
1
+ import { execFile, spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { createReadStream } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ const run = promisify(execFile);
7
+ /** Anchors every round to an immutable code state so round N+1 verdicts are not made against round N's memory. */
8
+ export const gitBaseline = async ({ cwd, subject }) => {
9
+ const git = async (args) => (await run('git', args, { cwd })).stdout;
10
+ try {
11
+ const commit = (await git(['rev-parse', 'HEAD'])).trim();
12
+ let dirtyHash;
13
+ const status = await git(['status', '--porcelain', '-z']);
14
+ if (status) {
15
+ const untracked = (await git(['ls-files', '--others', '--exclude-standard', '-z']))
16
+ .split('\0')
17
+ .filter(Boolean)
18
+ .sort();
19
+ // Hashed as a stream, never buffered. A working tree diff can be tens of megabytes, and a diff dropped for
20
+ // exceeding a buffer would leave a hash of nothing but file names - identical before and after an edit to
21
+ // those same files, which is exactly what `assertBaselineCurrent()` exists to catch.
22
+ const hash = createHash('sha256').update(status);
23
+ await hashStream(hash, spawn('git', ['diff', 'HEAD', '--binary'], { cwd, stdio: ['ignore', 'pipe', 'ignore'] }));
24
+ for (const relative of untracked) {
25
+ hash.update(relative).update('\0');
26
+ // A file removed during capture is already represented by status.
27
+ await hashStream(hash, createReadStream(path.join(cwd, relative))).catch(() => { });
28
+ }
29
+ dirtyHash = `sha256:${hash.digest('hex')}`;
30
+ }
31
+ // A commit range such as `main...feature` is only a name: either endpoint can move while HEAD and the
32
+ // working tree stay identical, which would silently change the code under review. Resolve both endpoints
33
+ // so the pinned identity covers the range, not just its spelling.
34
+ let rangeResolved;
35
+ if (subject.type === 'commit_range') {
36
+ const expression = subject.value.trim();
37
+ const endpoints = expression.includes('...')
38
+ ? expression.split('...')
39
+ : expression.includes('..')
40
+ ? expression.split('..')
41
+ : [expression];
42
+ const resolved = [];
43
+ for (const endpoint of endpoints) {
44
+ const name = endpoint.trim();
45
+ resolved.push(name
46
+ ? (await git(['rev-parse', '--verify', `${name}^{commit}`]).catch(() => '')).trim() ||
47
+ `unresolved:${name}`
48
+ : 'HEAD');
49
+ }
50
+ rangeResolved = resolved.join('...');
51
+ }
52
+ return {
53
+ vcs: 'git',
54
+ commit,
55
+ range: subject.type === 'commit_range' ? subject.value : undefined,
56
+ rangeResolved,
57
+ dirtyHash,
58
+ paths: subject.type === 'paths'
59
+ ? subject.value
60
+ .split(/[\n,]/)
61
+ .map(value => value.trim())
62
+ .filter(Boolean)
63
+ : []
64
+ };
65
+ }
66
+ catch {
67
+ // Not a git checkout (or git is missing). The hub still works; it just cannot prove code identity.
68
+ return { vcs: 'none', paths: [] };
69
+ }
70
+ };
71
+ /** Feeds one stream into a hash, rejecting on failure so a partial read can never pass as "nothing changed". */
72
+ function hashStream(hash, source) {
73
+ const process = 'stdout' in source ? source : undefined, stream = (process ? process.stdout : source);
74
+ return new Promise((resolve, reject) => {
75
+ if (!stream)
76
+ return reject(new Error('Stream is not readable'));
77
+ stream.on('data', chunk => hash.update(chunk));
78
+ stream.on('error', reject);
79
+ if (!process)
80
+ return void stream.on('end', resolve);
81
+ process.on('error', reject);
82
+ process.on('close', code => (code === 0 ? resolve() : reject(new Error(`git exited with ${code}`))));
83
+ });
84
+ }
@@ -86,7 +86,10 @@ export class CollabDispatcher {
86
86
  const existing = this.pending.get(participantId);
87
87
  if (existing)
88
88
  clearTimeout(existing);
89
- const timer = setTimeout(() => { this.pending.delete(participantId); void this.enqueue(sessionId, participantId, task); }, this.deps.delayMs ?? 150);
89
+ const timer = setTimeout(() => {
90
+ this.pending.delete(participantId);
91
+ void this.enqueue(sessionId, participantId, task);
92
+ }, this.deps.delayMs ?? 150);
90
93
  timer.unref?.();
91
94
  this.pending.set(participantId, timer);
92
95
  }
@@ -94,8 +97,10 @@ export class CollabDispatcher {
94
97
  enqueue(sessionId, participantId, task) {
95
98
  const previous = this.inFlight.get(participantId) ?? Promise.resolve();
96
99
  const job = previous.catch(() => { }).then(() => this.wake(sessionId, participantId, task));
97
- const tracked = job.finally(() => { if (this.inFlight.get(participantId) === tracked)
98
- this.inFlight.delete(participantId); });
100
+ const tracked = job.finally(() => {
101
+ if (this.inFlight.get(participantId) === tracked)
102
+ this.inFlight.delete(participantId);
103
+ });
99
104
  this.inFlight.set(participantId, tracked);
100
105
  return tracked;
101
106
  }
@@ -108,14 +113,21 @@ export class CollabDispatcher {
108
113
  return;
109
114
  }
110
115
  const agentId = participant.agentId;
111
- if (!agentId) { // only a pre-managed-only row can get here
116
+ if (!agentId) {
117
+ // only a pre-managed-only row can get here
112
118
  this.hub.logDispatch(sessionId, 'dispatch_failed', { participantId, task, reason: 'NO_AGENT_BOUND' });
113
119
  return;
114
120
  }
115
121
  if (participant.state !== 'active')
116
122
  return; // left or out of budget: a human has to act
117
- if (this.deps.isCollabAgent?.(agentId) === false) { // undefined: the check is not wired up
118
- this.hub.logDispatch(sessionId, 'dispatch_failed', { participantId, agentId, task, reason: 'COLLAB_PROFILE_REQUIRED' });
123
+ if (this.deps.isCollabAgent?.(agentId) === false) {
124
+ // undefined: the check is not wired up
125
+ this.hub.logDispatch(sessionId, 'dispatch_failed', {
126
+ participantId,
127
+ agentId,
128
+ task,
129
+ reason: 'COLLAB_PROFILE_REQUIRED'
130
+ });
119
131
  this.deps.log?.(`Collab dispatch skipped for ${participant.displayName}: the Agent is not profile=collab.`);
120
132
  return;
121
133
  }
@@ -137,7 +149,14 @@ export class CollabDispatcher {
137
149
  if (!terminal && this.hub.currentTaskFor(participantId) === 'wait') {
138
150
  this.hub.completeDelivery(participantId, task);
139
151
  this.delivered.delete(participantId);
140
- this.hub.logDispatch(sessionId, 'dispatch_skipped', { participantId, agentId, task, reason: 'NOTHING_OWED', phase: session.phase, round: session.round });
152
+ this.hub.logDispatch(sessionId, 'dispatch_skipped', {
153
+ participantId,
154
+ agentId,
155
+ task,
156
+ reason: 'NOTHING_OWED',
157
+ phase: session.phase,
158
+ round: session.round
159
+ });
141
160
  return;
142
161
  }
143
162
  // The bound agent is part of the identity of a delivery: after a rebind the *new* agent has received
@@ -152,7 +171,14 @@ export class CollabDispatcher {
152
171
  try {
153
172
  await this.deps.command(agentId, kind, toolBriefing({ session, participant, task }));
154
173
  this.delivered.set(participantId, key);
155
- this.hub.logDispatch(sessionId, 'agent_dispatched', { participantId, agentId, task, kind, phase: session.phase, round: session.round });
174
+ this.hub.logDispatch(sessionId, 'agent_dispatched', {
175
+ participantId,
176
+ agentId,
177
+ task,
178
+ kind,
179
+ phase: session.phase,
180
+ round: session.round
181
+ });
156
182
  // Normal work is acknowledged by collab_get_task, not here: followUp() returning means queued, not run.
157
183
  // A closing note has no tool collection step, so successful completion remains its acknowledgement.
158
184
  if (terminal) {
@@ -161,7 +187,12 @@ export class CollabDispatcher {
161
187
  }
162
188
  }
163
189
  catch (error) {
164
- this.hub.logDispatch(sessionId, 'dispatch_failed', { participantId, agentId, task, reason: error.message });
190
+ this.hub.logDispatch(sessionId, 'dispatch_failed', {
191
+ participantId,
192
+ agentId,
193
+ task,
194
+ reason: error.message
195
+ });
165
196
  this.deps.log?.(`Collab dispatch to agent ${agentId} failed: ${error.message}`);
166
197
  // The note did NOT arrive, so the item stays unacked: a restart retries it inside
167
198
  // CLOSING_NOTE_RETRY_WINDOW_MS.
@@ -179,7 +210,11 @@ export function toolBriefing(input) {
179
210
  `role: ${input.participant.role}; action: ${input.task}; working directory: ${input.session.cwd}`,
180
211
  `user request: ${input.session.subject.value}`,
181
212
  ...(input.session.subject.notes ? [`user notes: ${input.session.subject.notes}`] : []),
182
- ...(input.task === 'implement' ? [] : ['This is a non-mutating task. You may use bash to inspect (for example git diff/log), but do not create, delete, rename, or modify anything under the working directory, including redirected output, scratch files, generated files, caches, or formatter changes.']),
213
+ ...(input.task === 'implement'
214
+ ? []
215
+ : [
216
+ 'This is a non-mutating task. You may use bash to inspect (for example git diff/log), but do not create, delete, rename, or modify anything under the working directory, including redirected output, scratch files, generated files, caches, or formatter changes.'
217
+ ]),
183
218
  'Call collab_get_task first. It repeats the request, supplies the pinned baseline and activates the exact collab_submit_* tool; use that tool to record the complete result.',
184
219
  'Do not use curl, construct URLs, handle credentials, contact other agents, sleep, or poll. End your turn after submitting.'
185
220
  ].join('\n');