@venturewild/workspace 0.4.2 → 0.5.1

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 (56) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +112 -112
  3. package/package.json +85 -83
  4. package/server/bin/wild-workspace.mjs +1096 -1096
  5. package/server/src/account.mjs +114 -114
  6. package/server/src/agent-login.mjs +146 -146
  7. package/server/src/agent-readiness.mjs +200 -200
  8. package/server/src/agent.mjs +468 -468
  9. package/server/src/bazaar/core.mjs +790 -579
  10. package/server/src/bazaar/index.mjs +88 -75
  11. package/server/src/bazaar/mcp-server.mjs +417 -328
  12. package/server/src/bazaar/mock-tickup.mjs +97 -97
  13. package/server/src/bazaar/preview-server.mjs +95 -95
  14. package/server/src/bazaar/seed-recipes/customer-feedback-form/know-how.md +23 -23
  15. package/server/src/bazaar/seed-recipes/customer-feedback-form/recipe.json +24 -24
  16. package/server/src/bazaar/seed-recipes/landing-page-launch/know-how.md +29 -29
  17. package/server/src/bazaar/seed-recipes/landing-page-launch/recipe.json +25 -25
  18. package/server/src/bazaar/seed-recipes/personal-portfolio/know-how.md +21 -21
  19. package/server/src/bazaar/seed-recipes/personal-portfolio/recipe.json +24 -24
  20. package/server/src/bazaar/seed-recipes/receipt-sorter/know-how.md +31 -31
  21. package/server/src/bazaar/seed-recipes/receipt-sorter/recipe.json +25 -25
  22. package/server/src/bazaar/seed-recipes/tickup-hr-matching/know-how.md +79 -79
  23. package/server/src/bazaar/seed-recipes/tickup-hr-matching/recipe.json +40 -32
  24. package/server/src/canvas/core.mjs +446 -446
  25. package/server/src/canvas/index.mjs +42 -42
  26. package/server/src/canvas/mcp-server.mjs +253 -253
  27. package/server/src/canvas-rails.mjs +108 -108
  28. package/server/src/config.mjs +404 -404
  29. package/server/src/daemon-bin.mjs +110 -110
  30. package/server/src/daemon-supervisor.mjs +285 -285
  31. package/server/src/doctor.mjs +375 -375
  32. package/server/src/inbox.mjs +86 -86
  33. package/server/src/index.mjs +3279 -3032
  34. package/server/src/listings-rails.mjs +126 -0
  35. package/server/src/logpaths.mjs +98 -98
  36. package/server/src/observability.mjs +45 -45
  37. package/server/src/operator.mjs +92 -92
  38. package/server/src/pairing.mjs +137 -137
  39. package/server/src/service.mjs +515 -515
  40. package/server/src/session-reporter.mjs +201 -201
  41. package/server/src/settings.mjs +145 -145
  42. package/server/src/share.mjs +182 -182
  43. package/server/src/skills.mjs +213 -213
  44. package/server/src/supervisor.mjs +647 -647
  45. package/server/src/support-consent.mjs +133 -133
  46. package/server/src/sync.mjs +248 -248
  47. package/server/src/transcript.mjs +121 -121
  48. package/server/src/turn-mcp.mjs +46 -46
  49. package/server/src/usage.mjs +405 -405
  50. package/server/src/workspace-registry.mjs +295 -295
  51. package/server/src/workspaces.mjs +145 -135
  52. package/web/dist/assets/index-B8tHt7x-.css +32 -0
  53. package/web/dist/assets/index-BRY-IKaC.js +131 -0
  54. package/web/dist/index.html +2 -2
  55. package/web/dist/assets/index-DahRXN26.js +0 -91
  56. package/web/dist/assets/index-NXZN2LU2.css +0 -1
@@ -1,201 +1,201 @@
1
- // SessionReporter — the proactive, consented "is this user okay?" feed.
2
- //
3
- // WHY: the get-in path is self-serve, but if a real user gets stuck or their
4
- // install breaks, we were blind unless they ran `operator enable` and messaged
5
- // us — which makes us a bottleneck (docs/user-experience.md §5). This forwards a
6
- // LIVE, REDACTED stream of session events + install health to bmo-sync, keyed by
7
- // account, established at first load — so a stuck/broken user is never invisible
8
- // and never has to ask.
9
- //
10
- // PRIVACY — the load-bearing boundary: this feed carries WHAT happened (a turn
11
- // ran, tool X fired, an error, token/cost), NEVER the words. Chat text, tool
12
- // inputs, file contents, and paths are reduced to lengths/counts before anything
13
- // leaves the machine. Conversation *content* is a separate, separately-consented
14
- // channel (transcript.mjs). `redactEvent` is an ALLOWLIST projection — any event
15
- // type we don't explicitly model forwards only `{type}`, so a new event can
16
- // never leak by default. The matching test asserts a secret typed into a chat
17
- // turn never appears in the payload.
18
- //
19
- // Modeled on error-reporter.mjs (fire-and-forget, rate-limited, disable-able).
20
- // Gated by BOTH consent (user toggle) AND the shared WILD_WORKSPACE_NO_TELEMETRY
21
- // kill switch; inert without an accountToken (can't key it) or on a localhost
22
- // bmo-sync URL (dev).
23
-
24
- import os from 'node:os';
25
- import { APP_VERSION } from './config.mjs';
26
-
27
- function sanitizeUsage(u) {
28
- if (!u || typeof u !== 'object') return null;
29
- const out = {
30
- input_tokens: Number(u.input_tokens) || 0,
31
- output_tokens: Number(u.output_tokens) || 0,
32
- };
33
- if (typeof u.cost_usd === 'number') out.cost_usd = u.cost_usd;
34
- return out;
35
- }
36
-
37
- /**
38
- * Project one ActivityBus event to a SAFE shape. Allowlist by type; anything not
39
- * listed forwards only {type, ts, id}. NEVER returns chat text, tool inputs,
40
- * file paths, or file contents.
41
- */
42
- export function redactEvent(ev) {
43
- if (!ev || typeof ev !== 'object') return null;
44
- const base = { type: ev.type, ts: ev.ts, id: ev.id };
45
- if (ev.messageId) base.messageId = ev.messageId;
46
- switch (ev.type) {
47
- case 'chat-user':
48
- // The user's prompt — length only, never the words.
49
- return { ...base, textLen: typeof ev.text === 'string' ? ev.text.length : 0 };
50
- case 'chat-stream': {
51
- const c = ev.chunk || {};
52
- const safe = { ...base, chunkType: c.type };
53
- if (c.type === 'text' && typeof c.text === 'string') safe.textLen = c.text.length;
54
- // tool NAME is safe ("Edit", "Bash"); the tool INPUT is not — never forward it.
55
- if (c.type === 'tool-use') safe.tool = typeof c.name === 'string' ? c.name : c.tool || null;
56
- if (c.type === 'usage' && c.usage) safe.usage = sanitizeUsage(c.usage);
57
- if (c.type === 'error') safe.hasError = true; // the flag, not the message
58
- return safe;
59
- }
60
- case 'usage':
61
- return { ...base, usage: sanitizeUsage(ev.usage) };
62
- case 'chat-end':
63
- return { ...base, code: ev.code };
64
- case 'identity-changed':
65
- return { ...base, tone: ev.tone || null }; // tone only; drop the agent's name
66
- case 'onboarded':
67
- return { ...base, at: ev.at || null };
68
- case 'agent-changed':
69
- return { ...base, agentId: ev.agentId || null };
70
- case 'daemon-status':
71
- return { ...base, status: ev.status || null };
72
- case 'operator-action':
73
- return { ...base, action: ev.action || null }; // action verb, not its detail
74
- case 'inbox-change':
75
- return { ...base, count: Array.isArray(ev.snapshot) ? ev.snapshot.length : undefined };
76
- case 'presence-join':
77
- case 'presence-leave':
78
- case 'presence-focus':
79
- // sessionId + role are safe; `focus` can be a file path → dropped.
80
- return { ...base, sessionId: ev.sessionId, role: ev.role };
81
- default:
82
- // Unknown event → minimal envelope only. Privacy fails CLOSED.
83
- return { type: ev.type, ts: ev.ts, id: ev.id };
84
- }
85
- }
86
-
87
- const FLUSH_INTERVAL_MS = 15_000; // batch window
88
- const MAX_BATCH = 50; // flush early once this many buffer
89
- const MAX_BUFFER = 500; // hard cap — drop oldest beyond this
90
-
91
- export class SessionReporter {
92
- constructor({
93
- bmoSyncUrl,
94
- accountToken,
95
- slug = null,
96
- workspaceId = 'workspace',
97
- sessionId = null,
98
- enabled = true,
99
- endpointPath = '/api/telemetry',
100
- flushIntervalMs = FLUSH_INTERVAL_MS,
101
- maxBatch = MAX_BATCH,
102
- fetchImpl = (...a) => globalThis.fetch(...a),
103
- nowImpl = () => Date.now(),
104
- } = {}) {
105
- this.bmoSyncUrl = bmoSyncUrl ? bmoSyncUrl.replace(/\/$/, '') : null;
106
- this.accountToken = accountToken || null;
107
- this.slug = slug;
108
- this.workspaceId = workspaceId;
109
- this.sessionId = sessionId;
110
- this.endpointPath = endpointPath;
111
- this.flushIntervalMs = flushIntervalMs;
112
- this.maxBatch = maxBatch;
113
- this.fetchImpl = fetchImpl;
114
- this.nowImpl = nowImpl;
115
- this.buffer = [];
116
- this.timer = null;
117
- // Inert without a token, without a server, or on localhost (dev).
118
- this._capable =
119
- Boolean(this.accountToken) &&
120
- Boolean(this.bmoSyncUrl) &&
121
- !this.bmoSyncUrl.startsWith('http://127') &&
122
- !this.bmoSyncUrl.startsWith('http://localhost');
123
- this.enabled = enabled !== false && this._capable;
124
- }
125
-
126
- /** Live consent toggle — no restart needed when the user flips observability. */
127
- setEnabled(on) {
128
- this.enabled = Boolean(on) && this._capable;
129
- if (!this.enabled) this.buffer = [];
130
- }
131
-
132
- /** Feed one ActivityBus event. Redacts + buffers; flushes on size. No-op when off. */
133
- ingest(ev) {
134
- if (!this.enabled) return;
135
- const safe = redactEvent(ev);
136
- if (!safe) return;
137
- this.buffer.push(safe);
138
- if (this.buffer.length > MAX_BUFFER) this.buffer.splice(0, this.buffer.length - MAX_BUFFER);
139
- if (this.buffer.length >= this.maxBatch) this.flush();
140
- }
141
-
142
- /** POST the install-health snapshot alongside events (called by the supervisor path too). */
143
- envelope(events, extra = {}) {
144
- return {
145
- account_token: this.accountToken,
146
- slug: this.slug,
147
- workspace_id: this.workspaceId,
148
- session_id: this.sessionId,
149
- app_version: APP_VERSION,
150
- os: `${os.platform()}-${os.arch()}`,
151
- sent_at: Math.floor(this.nowImpl() / 1000),
152
- events,
153
- ...extra,
154
- };
155
- }
156
-
157
- /** Fire-and-forget flush of the current buffer. Never throws. */
158
- flush() {
159
- if (!this.enabled || !this.buffer.length) return;
160
- const events = this.buffer;
161
- this.buffer = [];
162
- this._post(this.envelope(events));
163
- }
164
-
165
- _post(body) {
166
- const url = `${this.bmoSyncUrl}${this.endpointPath}`;
167
- const ctrl = new AbortController();
168
- const timer = setTimeout(() => ctrl.abort(), 5000);
169
- if (timer.unref) timer.unref();
170
- let p;
171
- try {
172
- // Call synchronously so the request is observable without awaiting a tick.
173
- p = this.fetchImpl(url, {
174
- method: 'POST',
175
- headers: { 'content-type': 'application/json' },
176
- body: JSON.stringify(body),
177
- signal: ctrl.signal,
178
- });
179
- } catch {
180
- clearTimeout(timer);
181
- return; // telemetry must never break the user's path
182
- }
183
- Promise.resolve(p)
184
- .catch(() => {})
185
- .finally(() => clearTimeout(timer));
186
- }
187
-
188
- start() {
189
- if (this.timer || !this._capable) return;
190
- this.timer = setInterval(() => this.flush(), this.flushIntervalMs);
191
- if (this.timer.unref) this.timer.unref(); // never keep the process alive
192
- }
193
-
194
- stop() {
195
- if (this.timer) {
196
- clearInterval(this.timer);
197
- this.timer = null;
198
- }
199
- this.flush();
200
- }
201
- }
1
+ // SessionReporter — the proactive, consented "is this user okay?" feed.
2
+ //
3
+ // WHY: the get-in path is self-serve, but if a real user gets stuck or their
4
+ // install breaks, we were blind unless they ran `operator enable` and messaged
5
+ // us — which makes us a bottleneck (docs/user-experience.md §5). This forwards a
6
+ // LIVE, REDACTED stream of session events + install health to bmo-sync, keyed by
7
+ // account, established at first load — so a stuck/broken user is never invisible
8
+ // and never has to ask.
9
+ //
10
+ // PRIVACY — the load-bearing boundary: this feed carries WHAT happened (a turn
11
+ // ran, tool X fired, an error, token/cost), NEVER the words. Chat text, tool
12
+ // inputs, file contents, and paths are reduced to lengths/counts before anything
13
+ // leaves the machine. Conversation *content* is a separate, separately-consented
14
+ // channel (transcript.mjs). `redactEvent` is an ALLOWLIST projection — any event
15
+ // type we don't explicitly model forwards only `{type}`, so a new event can
16
+ // never leak by default. The matching test asserts a secret typed into a chat
17
+ // turn never appears in the payload.
18
+ //
19
+ // Modeled on error-reporter.mjs (fire-and-forget, rate-limited, disable-able).
20
+ // Gated by BOTH consent (user toggle) AND the shared WILD_WORKSPACE_NO_TELEMETRY
21
+ // kill switch; inert without an accountToken (can't key it) or on a localhost
22
+ // bmo-sync URL (dev).
23
+
24
+ import os from 'node:os';
25
+ import { APP_VERSION } from './config.mjs';
26
+
27
+ function sanitizeUsage(u) {
28
+ if (!u || typeof u !== 'object') return null;
29
+ const out = {
30
+ input_tokens: Number(u.input_tokens) || 0,
31
+ output_tokens: Number(u.output_tokens) || 0,
32
+ };
33
+ if (typeof u.cost_usd === 'number') out.cost_usd = u.cost_usd;
34
+ return out;
35
+ }
36
+
37
+ /**
38
+ * Project one ActivityBus event to a SAFE shape. Allowlist by type; anything not
39
+ * listed forwards only {type, ts, id}. NEVER returns chat text, tool inputs,
40
+ * file paths, or file contents.
41
+ */
42
+ export function redactEvent(ev) {
43
+ if (!ev || typeof ev !== 'object') return null;
44
+ const base = { type: ev.type, ts: ev.ts, id: ev.id };
45
+ if (ev.messageId) base.messageId = ev.messageId;
46
+ switch (ev.type) {
47
+ case 'chat-user':
48
+ // The user's prompt — length only, never the words.
49
+ return { ...base, textLen: typeof ev.text === 'string' ? ev.text.length : 0 };
50
+ case 'chat-stream': {
51
+ const c = ev.chunk || {};
52
+ const safe = { ...base, chunkType: c.type };
53
+ if (c.type === 'text' && typeof c.text === 'string') safe.textLen = c.text.length;
54
+ // tool NAME is safe ("Edit", "Bash"); the tool INPUT is not — never forward it.
55
+ if (c.type === 'tool-use') safe.tool = typeof c.name === 'string' ? c.name : c.tool || null;
56
+ if (c.type === 'usage' && c.usage) safe.usage = sanitizeUsage(c.usage);
57
+ if (c.type === 'error') safe.hasError = true; // the flag, not the message
58
+ return safe;
59
+ }
60
+ case 'usage':
61
+ return { ...base, usage: sanitizeUsage(ev.usage) };
62
+ case 'chat-end':
63
+ return { ...base, code: ev.code };
64
+ case 'identity-changed':
65
+ return { ...base, tone: ev.tone || null }; // tone only; drop the agent's name
66
+ case 'onboarded':
67
+ return { ...base, at: ev.at || null };
68
+ case 'agent-changed':
69
+ return { ...base, agentId: ev.agentId || null };
70
+ case 'daemon-status':
71
+ return { ...base, status: ev.status || null };
72
+ case 'operator-action':
73
+ return { ...base, action: ev.action || null }; // action verb, not its detail
74
+ case 'inbox-change':
75
+ return { ...base, count: Array.isArray(ev.snapshot) ? ev.snapshot.length : undefined };
76
+ case 'presence-join':
77
+ case 'presence-leave':
78
+ case 'presence-focus':
79
+ // sessionId + role are safe; `focus` can be a file path → dropped.
80
+ return { ...base, sessionId: ev.sessionId, role: ev.role };
81
+ default:
82
+ // Unknown event → minimal envelope only. Privacy fails CLOSED.
83
+ return { type: ev.type, ts: ev.ts, id: ev.id };
84
+ }
85
+ }
86
+
87
+ const FLUSH_INTERVAL_MS = 15_000; // batch window
88
+ const MAX_BATCH = 50; // flush early once this many buffer
89
+ const MAX_BUFFER = 500; // hard cap — drop oldest beyond this
90
+
91
+ export class SessionReporter {
92
+ constructor({
93
+ bmoSyncUrl,
94
+ accountToken,
95
+ slug = null,
96
+ workspaceId = 'workspace',
97
+ sessionId = null,
98
+ enabled = true,
99
+ endpointPath = '/api/telemetry',
100
+ flushIntervalMs = FLUSH_INTERVAL_MS,
101
+ maxBatch = MAX_BATCH,
102
+ fetchImpl = (...a) => globalThis.fetch(...a),
103
+ nowImpl = () => Date.now(),
104
+ } = {}) {
105
+ this.bmoSyncUrl = bmoSyncUrl ? bmoSyncUrl.replace(/\/$/, '') : null;
106
+ this.accountToken = accountToken || null;
107
+ this.slug = slug;
108
+ this.workspaceId = workspaceId;
109
+ this.sessionId = sessionId;
110
+ this.endpointPath = endpointPath;
111
+ this.flushIntervalMs = flushIntervalMs;
112
+ this.maxBatch = maxBatch;
113
+ this.fetchImpl = fetchImpl;
114
+ this.nowImpl = nowImpl;
115
+ this.buffer = [];
116
+ this.timer = null;
117
+ // Inert without a token, without a server, or on localhost (dev).
118
+ this._capable =
119
+ Boolean(this.accountToken) &&
120
+ Boolean(this.bmoSyncUrl) &&
121
+ !this.bmoSyncUrl.startsWith('http://127') &&
122
+ !this.bmoSyncUrl.startsWith('http://localhost');
123
+ this.enabled = enabled !== false && this._capable;
124
+ }
125
+
126
+ /** Live consent toggle — no restart needed when the user flips observability. */
127
+ setEnabled(on) {
128
+ this.enabled = Boolean(on) && this._capable;
129
+ if (!this.enabled) this.buffer = [];
130
+ }
131
+
132
+ /** Feed one ActivityBus event. Redacts + buffers; flushes on size. No-op when off. */
133
+ ingest(ev) {
134
+ if (!this.enabled) return;
135
+ const safe = redactEvent(ev);
136
+ if (!safe) return;
137
+ this.buffer.push(safe);
138
+ if (this.buffer.length > MAX_BUFFER) this.buffer.splice(0, this.buffer.length - MAX_BUFFER);
139
+ if (this.buffer.length >= this.maxBatch) this.flush();
140
+ }
141
+
142
+ /** POST the install-health snapshot alongside events (called by the supervisor path too). */
143
+ envelope(events, extra = {}) {
144
+ return {
145
+ account_token: this.accountToken,
146
+ slug: this.slug,
147
+ workspace_id: this.workspaceId,
148
+ session_id: this.sessionId,
149
+ app_version: APP_VERSION,
150
+ os: `${os.platform()}-${os.arch()}`,
151
+ sent_at: Math.floor(this.nowImpl() / 1000),
152
+ events,
153
+ ...extra,
154
+ };
155
+ }
156
+
157
+ /** Fire-and-forget flush of the current buffer. Never throws. */
158
+ flush() {
159
+ if (!this.enabled || !this.buffer.length) return;
160
+ const events = this.buffer;
161
+ this.buffer = [];
162
+ this._post(this.envelope(events));
163
+ }
164
+
165
+ _post(body) {
166
+ const url = `${this.bmoSyncUrl}${this.endpointPath}`;
167
+ const ctrl = new AbortController();
168
+ const timer = setTimeout(() => ctrl.abort(), 5000);
169
+ if (timer.unref) timer.unref();
170
+ let p;
171
+ try {
172
+ // Call synchronously so the request is observable without awaiting a tick.
173
+ p = this.fetchImpl(url, {
174
+ method: 'POST',
175
+ headers: { 'content-type': 'application/json' },
176
+ body: JSON.stringify(body),
177
+ signal: ctrl.signal,
178
+ });
179
+ } catch {
180
+ clearTimeout(timer);
181
+ return; // telemetry must never break the user's path
182
+ }
183
+ Promise.resolve(p)
184
+ .catch(() => {})
185
+ .finally(() => clearTimeout(timer));
186
+ }
187
+
188
+ start() {
189
+ if (this.timer || !this._capable) return;
190
+ this.timer = setInterval(() => this.flush(), this.flushIntervalMs);
191
+ if (this.timer.unref) this.timer.unref(); // never keep the process alive
192
+ }
193
+
194
+ stop() {
195
+ if (this.timer) {
196
+ clearInterval(this.timer);
197
+ this.timer = null;
198
+ }
199
+ this.flush();
200
+ }
201
+ }