@venturewild/workspace 0.5.3 → 0.6.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 (54) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +112 -112
  3. package/package.json +85 -85
  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 +974 -828
  10. package/server/src/bazaar/index.mjs +88 -88
  11. package/server/src/bazaar/mcp-server.mjs +429 -417
  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 -40
  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 +3332 -3279
  34. package/server/src/listings-rails.mjs +156 -126
  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-presence.mjs +0 -0
  51. package/server/src/workspace-registry.mjs +295 -295
  52. package/server/src/workspaces.mjs +145 -145
  53. package/web/dist/assets/{index-BxTh3dyq.js → index-ZYLNuQRa.js} +23 -23
  54. package/web/dist/index.html +1 -1
@@ -1,145 +1,145 @@
1
- // WorkspaceRails — the shared-workspace membership client (multi-host step 2).
2
- //
3
- // A thin client over bmo-sync's POST /api/workspaces/{create,list,members/add,
4
- // members/remove} (account-token self-authed, exactly like canvas-rails.mjs).
5
- // The caller (owner/member) is DERIVED server-side from the account token, so a
6
- // caller can only act as itself.
7
- //
8
- // Unlike CanvasRails (which degrades silently to a local cache), these are
9
- // operator/dev affordances driven by `wild-workspace workspace …`, so the
10
- // methods SURFACE the rails' error code + message instead of collapsing to a
11
- // bare false — the human running the command needs to see "slug_taken" /
12
- // "not_owner" / "no_such_account". Network failures still never throw: they
13
- // resolve to { ok:false, status:0, code:'unreachable' }.
14
- //
15
- // This is the FOUNDATION client — there is no App.jsx / lobby UI yet (that's the
16
- // lobby's job). The CLI is the only consumer.
17
-
18
- const DEFAULT_TIMEOUT_MS = 5000;
19
-
20
- export class WorkspaceRails {
21
- constructor({
22
- bmoSyncUrl,
23
- accountToken,
24
- timeoutMs = DEFAULT_TIMEOUT_MS,
25
- fetchImpl = (...a) => globalThis.fetch(...a),
26
- } = {}) {
27
- this.bmoSyncUrl = bmoSyncUrl ? bmoSyncUrl.replace(/\/$/, '') : null;
28
- this.accountToken = accountToken || null;
29
- this.timeoutMs = timeoutMs;
30
- this.fetchImpl = fetchImpl;
31
- // Inert without a token (can't key it) or without a server URL.
32
- this.capable = Boolean(this.accountToken) && Boolean(this.bmoSyncUrl);
33
- }
34
-
35
- /**
36
- * POST to a workspaces endpoint with the account token folded in.
37
- * @returns {Promise<{ok:boolean, status:number, data:object|null, code?:string, message?:string}>}
38
- * ok:true → 2xx; `data` is the parsed body.
39
- * ok:false → a 4xx/5xx (carries the rails' {code,message}) OR a transport
40
- * failure (status 0, code 'unreachable' / 'not_configured').
41
- */
42
- async _post(path, body) {
43
- if (!this.capable) {
44
- return { ok: false, status: 0, data: null, code: 'not_configured', message: 'not logged in / no rails URL' };
45
- }
46
- const ctrl = new AbortController();
47
- const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
48
- if (timer.unref) timer.unref();
49
- try {
50
- const r = await this.fetchImpl(`${this.bmoSyncUrl}${path}`, {
51
- method: 'POST',
52
- headers: { 'content-type': 'application/json' },
53
- body: JSON.stringify({ account_token: this.accountToken, ...body }),
54
- signal: ctrl.signal,
55
- });
56
- const data = await r.json().catch(() => null);
57
- if (r.ok) return { ok: true, status: r.status, data };
58
- return {
59
- ok: false,
60
- status: r.status,
61
- data,
62
- code: data?.code || `http_${r.status}`,
63
- message: data?.message || `HTTP ${r.status}`,
64
- };
65
- } catch (e) {
66
- return { ok: false, status: 0, data: null, code: 'unreachable', message: String(e?.message || e) };
67
- } finally {
68
- clearTimeout(timer);
69
- }
70
- }
71
-
72
- /** Create a shared workspace. `slug` is optional (the rails auto-suggest from name). */
73
- create(name, slug = null) {
74
- const body = { name };
75
- if (slug) body.slug = slug;
76
- return this._post('/api/workspaces/create', body);
77
- }
78
-
79
- /** The shared workspaces this account belongs to. */
80
- list() {
81
- return this._post('/api/workspaces/list', {});
82
- }
83
-
84
- /**
85
- * Add a member by email (owner-only). If the email has no account yet the
86
- * rails records a PENDING invite (`data.pending === true`) instead of an
87
- * active membership; the person claims it by signing in + Join.
88
- */
89
- addMember(slug, email) {
90
- return this._post('/api/workspaces/members/add', { slug, member_email: email });
91
- }
92
-
93
- /**
94
- * Claim membership at Join time. Activates a pending invite addressed to this
95
- * account's own VERIFIED email; a no-op for an already-active member. Returns
96
- * `{ok, data:{role, name, owner_email, claimed}}` or a 403 `not_a_member`.
97
- */
98
- claim(slug) {
99
- return this._post('/api/workspaces/claim', { slug });
100
- }
101
-
102
- /**
103
- * Mint a file-sync pass for a shared workspace this account belongs to
104
- * (Slice 3). Returns `{ok, data:{project_code, invite_code, expires_at}}` — the
105
- * daemon redeems `invite_code` to pair the folder and start replicating. The
106
- * project is provisioned lazily server-side; the code is stable, the invite is
107
- * one-shot (each call mints a fresh one). A non-member → 403 `not_a_member`.
108
- */
109
- syncCredential(slug) {
110
- return this._post('/api/workspaces/sync-credential', { slug });
111
- }
112
-
113
- /**
114
- * The roster of a shared workspace this account belongs to (R2). Returns
115
- * `{ok, data:{members:[{account_id,email,role,status,added_at}]}}` — active
116
- * members + pending invites (status:'invited'). `is_active_member`-gated; a
117
- * non-member → 403 `not_a_member`.
118
- */
119
- members(slug) {
120
- return this._post('/api/workspaces/members/list', { slug });
121
- }
122
-
123
- /** Remove a member by email or accountId (owner-only; the owner can't be removed). */
124
- removeMember(slug, ref) {
125
- const body = { slug };
126
- if (ref && typeof ref === 'object') {
127
- if (ref.accountId) body.account_id = ref.accountId;
128
- else if (ref.email) body.member_email = ref.email;
129
- } else if (typeof ref === 'string') {
130
- // Heuristic: an '@' means an email, otherwise treat it as an account id.
131
- if (ref.includes('@')) body.member_email = ref;
132
- else body.account_id = ref;
133
- }
134
- return this._post('/api/workspaces/members/remove', body);
135
- }
136
- }
137
-
138
- /** Build the client from server config + account (or an inert one when logged out). */
139
- export function createWorkspaceRails(config, account, fetchImpl) {
140
- return new WorkspaceRails({
141
- bmoSyncUrl: config?.bmoSyncServerUrl,
142
- accountToken: account?.accountToken,
143
- fetchImpl,
144
- });
145
- }
1
+ // WorkspaceRails — the shared-workspace membership client (multi-host step 2).
2
+ //
3
+ // A thin client over bmo-sync's POST /api/workspaces/{create,list,members/add,
4
+ // members/remove} (account-token self-authed, exactly like canvas-rails.mjs).
5
+ // The caller (owner/member) is DERIVED server-side from the account token, so a
6
+ // caller can only act as itself.
7
+ //
8
+ // Unlike CanvasRails (which degrades silently to a local cache), these are
9
+ // operator/dev affordances driven by `wild-workspace workspace …`, so the
10
+ // methods SURFACE the rails' error code + message instead of collapsing to a
11
+ // bare false — the human running the command needs to see "slug_taken" /
12
+ // "not_owner" / "no_such_account". Network failures still never throw: they
13
+ // resolve to { ok:false, status:0, code:'unreachable' }.
14
+ //
15
+ // This is the FOUNDATION client — there is no App.jsx / lobby UI yet (that's the
16
+ // lobby's job). The CLI is the only consumer.
17
+
18
+ const DEFAULT_TIMEOUT_MS = 5000;
19
+
20
+ export class WorkspaceRails {
21
+ constructor({
22
+ bmoSyncUrl,
23
+ accountToken,
24
+ timeoutMs = DEFAULT_TIMEOUT_MS,
25
+ fetchImpl = (...a) => globalThis.fetch(...a),
26
+ } = {}) {
27
+ this.bmoSyncUrl = bmoSyncUrl ? bmoSyncUrl.replace(/\/$/, '') : null;
28
+ this.accountToken = accountToken || null;
29
+ this.timeoutMs = timeoutMs;
30
+ this.fetchImpl = fetchImpl;
31
+ // Inert without a token (can't key it) or without a server URL.
32
+ this.capable = Boolean(this.accountToken) && Boolean(this.bmoSyncUrl);
33
+ }
34
+
35
+ /**
36
+ * POST to a workspaces endpoint with the account token folded in.
37
+ * @returns {Promise<{ok:boolean, status:number, data:object|null, code?:string, message?:string}>}
38
+ * ok:true → 2xx; `data` is the parsed body.
39
+ * ok:false → a 4xx/5xx (carries the rails' {code,message}) OR a transport
40
+ * failure (status 0, code 'unreachable' / 'not_configured').
41
+ */
42
+ async _post(path, body) {
43
+ if (!this.capable) {
44
+ return { ok: false, status: 0, data: null, code: 'not_configured', message: 'not logged in / no rails URL' };
45
+ }
46
+ const ctrl = new AbortController();
47
+ const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
48
+ if (timer.unref) timer.unref();
49
+ try {
50
+ const r = await this.fetchImpl(`${this.bmoSyncUrl}${path}`, {
51
+ method: 'POST',
52
+ headers: { 'content-type': 'application/json' },
53
+ body: JSON.stringify({ account_token: this.accountToken, ...body }),
54
+ signal: ctrl.signal,
55
+ });
56
+ const data = await r.json().catch(() => null);
57
+ if (r.ok) return { ok: true, status: r.status, data };
58
+ return {
59
+ ok: false,
60
+ status: r.status,
61
+ data,
62
+ code: data?.code || `http_${r.status}`,
63
+ message: data?.message || `HTTP ${r.status}`,
64
+ };
65
+ } catch (e) {
66
+ return { ok: false, status: 0, data: null, code: 'unreachable', message: String(e?.message || e) };
67
+ } finally {
68
+ clearTimeout(timer);
69
+ }
70
+ }
71
+
72
+ /** Create a shared workspace. `slug` is optional (the rails auto-suggest from name). */
73
+ create(name, slug = null) {
74
+ const body = { name };
75
+ if (slug) body.slug = slug;
76
+ return this._post('/api/workspaces/create', body);
77
+ }
78
+
79
+ /** The shared workspaces this account belongs to. */
80
+ list() {
81
+ return this._post('/api/workspaces/list', {});
82
+ }
83
+
84
+ /**
85
+ * Add a member by email (owner-only). If the email has no account yet the
86
+ * rails records a PENDING invite (`data.pending === true`) instead of an
87
+ * active membership; the person claims it by signing in + Join.
88
+ */
89
+ addMember(slug, email) {
90
+ return this._post('/api/workspaces/members/add', { slug, member_email: email });
91
+ }
92
+
93
+ /**
94
+ * Claim membership at Join time. Activates a pending invite addressed to this
95
+ * account's own VERIFIED email; a no-op for an already-active member. Returns
96
+ * `{ok, data:{role, name, owner_email, claimed}}` or a 403 `not_a_member`.
97
+ */
98
+ claim(slug) {
99
+ return this._post('/api/workspaces/claim', { slug });
100
+ }
101
+
102
+ /**
103
+ * Mint a file-sync pass for a shared workspace this account belongs to
104
+ * (Slice 3). Returns `{ok, data:{project_code, invite_code, expires_at}}` — the
105
+ * daemon redeems `invite_code` to pair the folder and start replicating. The
106
+ * project is provisioned lazily server-side; the code is stable, the invite is
107
+ * one-shot (each call mints a fresh one). A non-member → 403 `not_a_member`.
108
+ */
109
+ syncCredential(slug) {
110
+ return this._post('/api/workspaces/sync-credential', { slug });
111
+ }
112
+
113
+ /**
114
+ * The roster of a shared workspace this account belongs to (R2). Returns
115
+ * `{ok, data:{members:[{account_id,email,role,status,added_at}]}}` — active
116
+ * members + pending invites (status:'invited'). `is_active_member`-gated; a
117
+ * non-member → 403 `not_a_member`.
118
+ */
119
+ members(slug) {
120
+ return this._post('/api/workspaces/members/list', { slug });
121
+ }
122
+
123
+ /** Remove a member by email or accountId (owner-only; the owner can't be removed). */
124
+ removeMember(slug, ref) {
125
+ const body = { slug };
126
+ if (ref && typeof ref === 'object') {
127
+ if (ref.accountId) body.account_id = ref.accountId;
128
+ else if (ref.email) body.member_email = ref.email;
129
+ } else if (typeof ref === 'string') {
130
+ // Heuristic: an '@' means an email, otherwise treat it as an account id.
131
+ if (ref.includes('@')) body.member_email = ref;
132
+ else body.account_id = ref;
133
+ }
134
+ return this._post('/api/workspaces/members/remove', body);
135
+ }
136
+ }
137
+
138
+ /** Build the client from server config + account (or an inert one when logged out). */
139
+ export function createWorkspaceRails(config, account, fetchImpl) {
140
+ return new WorkspaceRails({
141
+ bmoSyncUrl: config?.bmoSyncServerUrl,
142
+ accountToken: account?.accountToken,
143
+ fetchImpl,
144
+ });
145
+ }