@skillcdn/console 0.1.6 → 0.1.7

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 (88) hide show
  1. package/README.md +50 -2
  2. package/SKILLCDN.md +29 -0
  3. package/dist/cli/cli.d.ts +3 -0
  4. package/dist/cli/cli.d.ts.map +1 -1
  5. package/dist/cli/cli.js +61 -0
  6. package/dist/cli/cli.js.map +1 -1
  7. package/dist/cli/help.d.ts.map +1 -1
  8. package/dist/cli/help.js +5 -0
  9. package/dist/cli/help.js.map +1 -1
  10. package/dist/cli/main.js +2 -0
  11. package/dist/cli/main.js.map +1 -1
  12. package/dist/cli/serve.d.ts +37 -0
  13. package/dist/cli/serve.d.ts.map +1 -0
  14. package/dist/cli/serve.js +218 -0
  15. package/dist/cli/serve.js.map +1 -0
  16. package/dist/console.d.ts +6 -0
  17. package/dist/console.d.ts.map +1 -1
  18. package/dist/console.js +78 -13
  19. package/dist/console.js.map +1 -1
  20. package/dist/data.d.ts.map +1 -1
  21. package/dist/data.js +5 -1
  22. package/dist/data.js.map +1 -1
  23. package/dist/i18n/en.d.ts +4 -0
  24. package/dist/i18n/en.d.ts.map +1 -1
  25. package/dist/i18n/en.js +4 -0
  26. package/dist/i18n/en.js.map +1 -1
  27. package/dist/i18n/ko.d.ts.map +1 -1
  28. package/dist/i18n/ko.js +4 -0
  29. package/dist/i18n/ko.js.map +1 -1
  30. package/dist/schemas.d.ts +1 -0
  31. package/dist/schemas.d.ts.map +1 -1
  32. package/dist/schemas.js +5 -0
  33. package/dist/schemas.js.map +1 -1
  34. package/dist/web.d.ts +16 -0
  35. package/dist/web.d.ts.map +1 -0
  36. package/dist/web.js +149 -0
  37. package/dist/web.js.map +1 -0
  38. package/docs/specs/cli.md +92 -0
  39. package/docs/specs/rest.md +220 -0
  40. package/package.json +14 -3
  41. package/skills/building-a-console/SKILL.md +79 -0
  42. package/skills/working-the-board/SKILL.md +78 -0
  43. package/src/api.ts +235 -0
  44. package/src/cli/cli.ts +1114 -0
  45. package/src/cli/credentials.ts +68 -0
  46. package/src/cli/directory.ts +59 -0
  47. package/src/cli/format.ts +284 -0
  48. package/src/cli/help.ts +326 -0
  49. package/src/cli/main.ts +39 -0
  50. package/src/cli/serve.ts +276 -0
  51. package/src/cli/terminal.ts +28 -0
  52. package/src/client.ts +533 -0
  53. package/src/components/board.tsx +112 -0
  54. package/src/components/connect.tsx +126 -0
  55. package/src/components/decision-form.tsx +136 -0
  56. package/src/components/decision-list.tsx +278 -0
  57. package/src/components/documents.tsx +569 -0
  58. package/src/components/event-feed.tsx +129 -0
  59. package/src/components/markdown.tsx +72 -0
  60. package/src/components/people.tsx +59 -0
  61. package/src/components/projects.tsx +364 -0
  62. package/src/components/runs.tsx +175 -0
  63. package/src/components/shell.tsx +284 -0
  64. package/src/components/sign-in.tsx +61 -0
  65. package/src/components/skills.tsx +78 -0
  66. package/src/components/task-form.tsx +229 -0
  67. package/src/components/task-view.tsx +272 -0
  68. package/src/components/tokens.tsx +223 -0
  69. package/src/components/ui.tsx +219 -0
  70. package/src/connect-page.tsx +148 -0
  71. package/src/connect.ts +27 -0
  72. package/src/console.tsx +1166 -0
  73. package/src/data.ts +493 -0
  74. package/src/docs-page.tsx +334 -0
  75. package/src/documents.ts +76 -0
  76. package/src/i18n/en.ts +537 -0
  77. package/src/i18n/index.ts +76 -0
  78. package/src/i18n/ko.ts +511 -0
  79. package/src/index.ts +142 -0
  80. package/src/limits.ts +61 -0
  81. package/src/router.ts +295 -0
  82. package/src/routes.ts +106 -0
  83. package/src/schemas.ts +776 -0
  84. package/src/styles/console.css +2432 -0
  85. package/src/text.ts +33 -0
  86. package/src/use-action.ts +37 -0
  87. package/src/vocabulary.ts +109 -0
  88. package/src/web.ts +170 -0
package/src/data.ts ADDED
@@ -0,0 +1,493 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import {
3
+ ApiError,
4
+ type ConsoleClient,
5
+ type DocumentFilter,
6
+ EVENT_KINDS,
7
+ type FileUpload,
8
+ type RestAnswerInput,
9
+ type RestConnectRequest,
10
+ type RestDecision,
11
+ type RestDecisionInput,
12
+ type RestDecisionPatch,
13
+ type RestDocument,
14
+ type RestDocumentInput,
15
+ type RestDocuments,
16
+ type RestEvent,
17
+ type RestMe,
18
+ type RestMember,
19
+ type RestMemberInput,
20
+ type RestMemberPatch,
21
+ type RestMePatch,
22
+ type RestPerson,
23
+ type RestPersonPatch,
24
+ type RestProject,
25
+ type RestProjectInput,
26
+ type RestProjectPatch,
27
+ type RestRun,
28
+ type RestSkills,
29
+ type RestTask,
30
+ type RestTaskInput,
31
+ type RestTaskPatch,
32
+ type RestToken,
33
+ type RestTokenCreated,
34
+ type RestTokenInput,
35
+ type RestTokens,
36
+ type RestVersion,
37
+ type RestVersions,
38
+ restEventSchema,
39
+ } from "./api.js";
40
+
41
+ // What the console shows, and how it stays current: the workspace (who is signed in, the
42
+ // projects, the people, the tokens) is loaded once; the project the page is on is loaded whole,
43
+ // its feed from the beginning, and from then on the project's stream says when something
44
+ // changed: on every event the lists are loaded again, which is the boring way to be right.
45
+
46
+ /** How many events the feed keeps on the page. */
47
+ const FEED_KEEP = 200;
48
+ /** How many pages of the feed are read at the start before the stream takes over. */
49
+ const FEED_MAX_PAGES = 20;
50
+ /** How long changes that arrive together are gathered before the lists are loaded once. */
51
+ const RELOAD_DELAY_MS = 150;
52
+
53
+ export interface ConsoleActions {
54
+ /** Makes a project, whose owner the person becomes. */
55
+ createProject(input: RestProjectInput): Promise<RestProject>;
56
+ /** Changes the settings of the project the page is on; for an owner. */
57
+ updateProject(patch: RestProjectPatch): Promise<RestProject>;
58
+ addMember(input: RestMemberInput): Promise<RestMember>;
59
+ updateMember(personId: string, patch: RestMemberPatch): Promise<RestMember>;
60
+ removeMember(personId: string): Promise<void>;
61
+ createTask(input: RestTaskInput): Promise<RestTask>;
62
+ updateTask(id: string, patch: RestTaskPatch): Promise<RestTask>;
63
+ raiseDecision(input: RestDecisionInput): Promise<RestDecision>;
64
+ answerDecision(id: string, input: RestAnswerInput): Promise<RestDecision>;
65
+ /** Grows a decision's record: its context, or what followed. */
66
+ updateDecision(id: string, patch: RestDecisionPatch): Promise<RestDecision>;
67
+ /** A folder's pages and folders, or the pages a search finds, in the project the page is on. */
68
+ documents(filter?: DocumentFilter, signal?: AbortSignal): Promise<RestDocuments>;
69
+ document(path: string, signal?: AbortSignal): Promise<RestDocument>;
70
+ writeDocument(path: string, input: RestDocumentInput): Promise<RestDocument>;
71
+ archiveDocument(path: string): Promise<RestDocument>;
72
+ restoreDocument(path: string): Promise<RestDocument>;
73
+ documentVersions(path: string, signal?: AbortSignal): Promise<RestVersions>;
74
+ documentVersion(path: string, number: number, signal?: AbortSignal): Promise<RestVersion>;
75
+ attachDocumentFile(path: string, input: FileUpload): Promise<RestDocument>;
76
+ /** Changes what a person is; for an administrator. */
77
+ updatePerson(id: string, patch: RestPersonPatch): Promise<RestPerson>;
78
+ /** Keeps the person's own choice of language (ADR-0012). */
79
+ updateMe(patch: RestMePatch): Promise<RestMe>;
80
+ /** Marks a run that will not come back as abandoned. */
81
+ abandonRun(id: string): Promise<RestRun>;
82
+ /** Everything that happened to one task of the project, oldest first. */
83
+ taskHistory(taskId: string, signal?: AbortSignal): Promise<RestEvent[]>;
84
+ /** Makes a token for whoever is signed in; the answer carries the secret, this once. */
85
+ createToken(input: RestTokenInput): Promise<RestTokenCreated>;
86
+ revokeToken(id: string): Promise<void>;
87
+ /** What asks to connect under a code (ADR-0011), for the person who approves it. */
88
+ connectRequest(code: string, signal?: AbortSignal): Promise<RestConnectRequest>;
89
+ approveConnection(code: string, input: RestTokenInput): Promise<RestConnectRequest>;
90
+ denyConnection(code: string): Promise<void>;
91
+ /** The tokens of another person, and the way to take one away: for an administrator. */
92
+ personTokens(personId: string, signal?: AbortSignal): Promise<RestTokens>;
93
+ revokePersonToken(personId: string, tokenId: string): Promise<void>;
94
+ signOut(): Promise<void>;
95
+ }
96
+
97
+ export interface ConsoleData {
98
+ /** Who is signed in and what the board is called; `undefined` until the server has answered. */
99
+ readonly me: RestMe | undefined;
100
+ /** What went wrong asking who is signed in, for a person. */
101
+ readonly error: string | undefined;
102
+ /** True once the workspace has been loaded for the first time: the projects, the people, the tokens. */
103
+ readonly loaded: boolean;
104
+ /** The projects the person may see, by name. */
105
+ readonly projects: readonly RestProject[];
106
+ readonly people: readonly RestPerson[];
107
+ /** The tokens of whoever is signed in, newest first. */
108
+ readonly tokens: readonly RestToken[];
109
+ /** The project the page is on, once loaded; `undefined` while loading, or when the page is on none. */
110
+ readonly project: RestProject | undefined;
111
+ /** True once the project the page is on has been loaded; false while it loads, or when there is none. */
112
+ readonly projectLoaded: boolean;
113
+ /** Why the project could not be loaded: not one the person may see, for instance. */
114
+ readonly projectError: string | undefined;
115
+ /** Whether the project's stream of events is connected. */
116
+ readonly live: boolean;
117
+ readonly tasks: readonly RestTask[];
118
+ readonly decisions: readonly RestDecision[];
119
+ /** The runs of the project, newest first. */
120
+ readonly runs: readonly RestRun[];
121
+ /** The project's events, oldest first. */
122
+ readonly events: readonly RestEvent[];
123
+ /** Those listed in the project, by login. */
124
+ readonly members: readonly RestMember[];
125
+ /** The project's skills, as the console answered; `undefined` until it has. */
126
+ readonly skills: RestSkills | undefined;
127
+ readonly actions: ConsoleActions;
128
+ /** Asks everything again, from who is signed in on. */
129
+ reload(): void;
130
+ }
131
+
132
+ /** The whole feed of a project from the beginning, page by page, bounded. */
133
+ async function readFeed(
134
+ client: ConsoleClient,
135
+ key: string,
136
+ signal: AbortSignal,
137
+ ): Promise<RestEvent[]> {
138
+ const events: RestEvent[] = [];
139
+ let after = 0;
140
+ for (let page = 0; page < FEED_MAX_PAGES; page += 1) {
141
+ const result = await client.project(key).events(after, {}, signal);
142
+ events.push(...result.items);
143
+ after = result.items.at(-1)?.id ?? after;
144
+ if (!result.more) {
145
+ break;
146
+ }
147
+ }
148
+ return events.slice(-FEED_KEEP);
149
+ }
150
+
151
+ const wordsOf = (failure: unknown, fallback: string): string =>
152
+ failure instanceof ApiError ? failure.message : fallback;
153
+
154
+ /**
155
+ * The data of the console: the workspace, and the project `projectKey` names, kept current.
156
+ * A composition of its own calls this with the key of the project its page is on, or none.
157
+ */
158
+ export function useConsoleData(client: ConsoleClient, projectKey: string | undefined): ConsoleData {
159
+ const [me, setMe] = useState<RestMe | undefined>(undefined);
160
+ const [error, setError] = useState<string | undefined>(undefined);
161
+ const [loaded, setLoaded] = useState(false);
162
+ const [projects, setProjects] = useState<readonly RestProject[]>([]);
163
+ const [people, setPeople] = useState<readonly RestPerson[]>([]);
164
+ const [tokens, setTokens] = useState<readonly RestToken[]>([]);
165
+ const [project, setProject] = useState<RestProject | undefined>(undefined);
166
+ const [projectLoaded, setProjectLoaded] = useState(false);
167
+ const [projectError, setProjectError] = useState<string | undefined>(undefined);
168
+ const [live, setLive] = useState(false);
169
+ const [tasks, setTasks] = useState<readonly RestTask[]>([]);
170
+ const [decisions, setDecisions] = useState<readonly RestDecision[]>([]);
171
+ const [runs, setRuns] = useState<readonly RestRun[]>([]);
172
+ const [events, setEvents] = useState<readonly RestEvent[]>([]);
173
+ const [members, setMembers] = useState<readonly RestMember[]>([]);
174
+ const [skills, setSkills] = useState<RestSkills | undefined>(undefined);
175
+ const [generation, setGeneration] = useState(0);
176
+ const lastEvent = useRef(0);
177
+ const signedIn = me?.person !== null && me !== undefined;
178
+
179
+ const reload = useCallback(() => setGeneration((current) => current + 1), []);
180
+
181
+ /** Loads the workspace's lists again. What fails stays as it was. */
182
+ const refreshWorkspace = useCallback(
183
+ async (signal?: AbortSignal) => {
184
+ const [nextProjects, nextPeople] = await Promise.all([
185
+ client.projects(signal),
186
+ client.people(signal),
187
+ ]);
188
+ if (signal?.aborted === true) {
189
+ return;
190
+ }
191
+ setProjects(nextProjects.items);
192
+ setPeople(nextPeople.items);
193
+ },
194
+ [client],
195
+ );
196
+
197
+ /** Loads the project's lists again. What fails stays as it was; the next event asks again. */
198
+ const refreshProject = useCallback(
199
+ async (signal?: AbortSignal) => {
200
+ if (projectKey === undefined) {
201
+ return;
202
+ }
203
+ const scope = client.project(projectKey);
204
+ const [nextProject, nextTasks, nextDecisions, nextRuns, nextMembers] = await Promise.all([
205
+ scope.get(signal),
206
+ scope.tasks(undefined, signal),
207
+ scope.decisions(undefined, signal),
208
+ scope.runs(undefined, signal),
209
+ scope.members(signal),
210
+ ]);
211
+ if (signal?.aborted === true) {
212
+ return;
213
+ }
214
+ setProject(nextProject);
215
+ setTasks(nextTasks.items);
216
+ setDecisions(nextDecisions.items);
217
+ setRuns(nextRuns.items);
218
+ setMembers(nextMembers.items);
219
+ },
220
+ [client, projectKey],
221
+ );
222
+
223
+ // Who is signed in, then the workspace: the projects, the people, the person's tokens.
224
+ // biome-ignore lint/correctness/useExhaustiveDependencies: `generation` is the trigger
225
+ useEffect(() => {
226
+ const controller = new AbortController();
227
+ const { signal } = controller;
228
+ setError(undefined);
229
+ const run = async () => {
230
+ let answer: RestMe;
231
+ try {
232
+ answer = await client.me(signal);
233
+ } catch (failure) {
234
+ if (!signal.aborted) {
235
+ setError(wordsOf(failure, "The server could not be reached."));
236
+ }
237
+ return;
238
+ }
239
+ if (signal.aborted) {
240
+ return;
241
+ }
242
+ setMe(answer);
243
+ if (answer.person === null) {
244
+ setLoaded(false);
245
+ return;
246
+ }
247
+ try {
248
+ const [, mine] = await Promise.all([
249
+ refreshWorkspace(signal),
250
+ // A token cannot list tokens: a console that holds one has none to show.
251
+ answer.agent === null ? client.tokens(signal) : Promise.resolve({ items: [] }),
252
+ ]);
253
+ if (signal.aborted) {
254
+ return;
255
+ }
256
+ setTokens(mine.items);
257
+ setLoaded(true);
258
+ } catch (failure) {
259
+ if (!signal.aborted) {
260
+ setError(wordsOf(failure, "The workspace could not be loaded."));
261
+ }
262
+ }
263
+ };
264
+ void run();
265
+ return () => controller.abort();
266
+ }, [client, generation, refreshWorkspace]);
267
+
268
+ // The project the page is on: its lists, its feed from the beginning, and its stream from
269
+ // where the feed ends. Another project, or none, starts over.
270
+ // biome-ignore lint/correctness/useExhaustiveDependencies: `generation` is the trigger
271
+ useEffect(() => {
272
+ setProject(undefined);
273
+ setProjectLoaded(false);
274
+ setProjectError(undefined);
275
+ setTasks([]);
276
+ setDecisions([]);
277
+ setRuns([]);
278
+ setEvents([]);
279
+ setMembers([]);
280
+ setSkills(undefined);
281
+ setLive(false);
282
+ if (projectKey === undefined || !signedIn) {
283
+ return;
284
+ }
285
+ const controller = new AbortController();
286
+ const { signal } = controller;
287
+ let source: EventSource | undefined;
288
+ let reloadTimer: ReturnType<typeof setTimeout> | undefined;
289
+ const scope = client.project(projectKey);
290
+ const run = async () => {
291
+ try {
292
+ const [, feed] = await Promise.all([
293
+ refreshProject(signal),
294
+ readFeed(client, projectKey, signal),
295
+ ]);
296
+ if (signal.aborted) {
297
+ return;
298
+ }
299
+ setEvents(feed);
300
+ lastEvent.current = feed.at(-1)?.id ?? 0;
301
+ setProjectLoaded(true);
302
+ } catch (failure) {
303
+ if (!signal.aborted) {
304
+ setProjectError(wordsOf(failure, "The project could not be loaded."));
305
+ }
306
+ return;
307
+ }
308
+ // The skills are someone else's to serve: asked for on their own, so that the board
309
+ // stands without them.
310
+ scope
311
+ .skills(signal)
312
+ .then((answer) => {
313
+ if (!signal.aborted) {
314
+ setSkills(answer);
315
+ }
316
+ })
317
+ .catch(() => undefined);
318
+ if (typeof EventSource === "undefined") {
319
+ return;
320
+ }
321
+ source = new EventSource(scope.eventStreamUrl(lastEvent.current));
322
+ source.onopen = () => setLive(true);
323
+ source.onerror = () => setLive(false);
324
+ const arrived = (message: MessageEvent) => {
325
+ const parsed = restEventSchema.safeParse(JSON.parse(String(message.data)));
326
+ if (!parsed.success || parsed.data.id <= lastEvent.current) {
327
+ return;
328
+ }
329
+ lastEvent.current = parsed.data.id;
330
+ const event = parsed.data;
331
+ setEvents((current) => [...current, event].slice(-FEED_KEEP));
332
+ clearTimeout(reloadTimer);
333
+ reloadTimer = setTimeout(() => {
334
+ refreshProject(signal).catch(() => undefined);
335
+ if (event.kind.startsWith("project.")) {
336
+ refreshWorkspace(signal).catch(() => undefined);
337
+ }
338
+ }, RELOAD_DELAY_MS);
339
+ };
340
+ for (const kind of EVENT_KINDS) {
341
+ source.addEventListener(kind, arrived);
342
+ }
343
+ };
344
+ void run();
345
+ return () => {
346
+ controller.abort();
347
+ clearTimeout(reloadTimer);
348
+ source?.close();
349
+ setLive(false);
350
+ };
351
+ }, [client, projectKey, signedIn, generation, refreshProject, refreshWorkspace]);
352
+
353
+ const actions = useMemo<ConsoleActions>(() => {
354
+ /** The project the page is on, for an action that needs one. */
355
+ const scope = () => {
356
+ if (projectKey === undefined) {
357
+ throw new ApiError(0, "no_project", "The page is on no project.");
358
+ }
359
+ return client.project(projectKey);
360
+ };
361
+ return {
362
+ async createProject(input) {
363
+ const made = await client.createProject(input);
364
+ await refreshWorkspace();
365
+ return made;
366
+ },
367
+ async updateProject(patch) {
368
+ const changed = await scope().update(patch);
369
+ setProject(changed);
370
+ await refreshWorkspace();
371
+ return changed;
372
+ },
373
+ async addMember(input) {
374
+ const member = await scope().addMember(input);
375
+ await refreshProject();
376
+ return member;
377
+ },
378
+ async updateMember(personId, patch) {
379
+ const member = await scope().updateMember(personId, patch);
380
+ await refreshProject();
381
+ return member;
382
+ },
383
+ async removeMember(personId) {
384
+ await scope().removeMember(personId);
385
+ await refreshProject();
386
+ },
387
+ async createTask(input) {
388
+ const task = await scope().createTask(input);
389
+ await refreshProject();
390
+ return task;
391
+ },
392
+ async updateTask(id, patch) {
393
+ const task = await scope().updateTask(id, patch);
394
+ setTasks((current) => current.map((candidate) => (candidate.id === id ? task : candidate)));
395
+ return task;
396
+ },
397
+ async raiseDecision(input) {
398
+ const decision = await scope().raiseDecision(input);
399
+ await refreshProject();
400
+ return decision;
401
+ },
402
+ async answerDecision(id, input) {
403
+ const decision = await scope().answerDecision(id, input);
404
+ await refreshProject();
405
+ return decision;
406
+ },
407
+ async updateDecision(id, patch) {
408
+ const decision = await scope().updateDecision(id, patch);
409
+ setDecisions((current) =>
410
+ current.map((candidate) => (candidate.id === id ? decision : candidate)),
411
+ );
412
+ return decision;
413
+ },
414
+ documents: (filter, signal) => scope().documents(filter, signal),
415
+ document: (path, signal) => scope().document(path, signal),
416
+ writeDocument: (path, input) => scope().writeDocument(path, input),
417
+ archiveDocument: (path) => scope().archiveDocument(path),
418
+ restoreDocument: (path) => scope().restoreDocument(path),
419
+ documentVersions: (path, signal) => scope().documentVersions(path, signal),
420
+ documentVersion: (path, number, signal) => scope().documentVersion(path, number, signal),
421
+ attachDocumentFile: (path, input) => scope().attachFile(path, input),
422
+ async updatePerson(id, patch) {
423
+ const person = await client.updatePerson(id, patch);
424
+ setMe((current) => (current?.person?.id === id ? { ...current, person } : current));
425
+ await refreshWorkspace();
426
+ return person;
427
+ },
428
+ async updateMe(patch) {
429
+ const answer = await client.updateMe(patch);
430
+ setMe(answer);
431
+ return answer;
432
+ },
433
+ async abandonRun(id) {
434
+ const run = await scope().endRun(id, { status: "abandoned" });
435
+ await refreshProject();
436
+ return run;
437
+ },
438
+ async taskHistory(taskId, signal) {
439
+ const history: RestEvent[] = [];
440
+ let after = 0;
441
+ for (let page = 0; page < FEED_MAX_PAGES; page += 1) {
442
+ const result = await scope().events(after, { task: taskId }, signal);
443
+ history.push(...result.items);
444
+ after = result.items.at(-1)?.id ?? after;
445
+ if (!result.more) {
446
+ break;
447
+ }
448
+ }
449
+ return history;
450
+ },
451
+ async createToken(input) {
452
+ const made = await client.createToken(input);
453
+ setTokens((await client.tokens()).items);
454
+ return made;
455
+ },
456
+ async revokeToken(id) {
457
+ await client.revokeToken(id);
458
+ setTokens((current) => current.filter((token) => token.id !== id));
459
+ },
460
+ connectRequest: (code, signal) => client.connectRequest(code, signal),
461
+ approveConnection: (code, input) => client.approveConnection(code, input),
462
+ denyConnection: (code) => client.denyConnection(code),
463
+ personTokens: (personId, signal) => client.personTokens(personId, signal),
464
+ revokePersonToken: (personId, tokenId) => client.revokePersonToken(personId, tokenId),
465
+ async signOut() {
466
+ await client.signOut();
467
+ setMe((current) => (current === undefined ? undefined : { ...current, person: null }));
468
+ setLoaded(false);
469
+ },
470
+ };
471
+ }, [client, projectKey, refreshProject, refreshWorkspace]);
472
+
473
+ return {
474
+ me,
475
+ error,
476
+ loaded,
477
+ projects,
478
+ people,
479
+ tokens,
480
+ project,
481
+ projectLoaded,
482
+ projectError,
483
+ live,
484
+ tasks,
485
+ decisions,
486
+ runs,
487
+ events,
488
+ members,
489
+ skills,
490
+ actions,
491
+ reload,
492
+ };
493
+ }