opensoma 0.5.1 → 0.7.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 (149) hide show
  1. package/dist/package.json +5 -1
  2. package/dist/src/agent-browser-launcher.d.ts +43 -0
  3. package/dist/src/agent-browser-launcher.d.ts.map +1 -0
  4. package/dist/src/agent-browser-launcher.js +97 -0
  5. package/dist/src/agent-browser-launcher.js.map +1 -0
  6. package/dist/src/cli.d.ts.map +1 -1
  7. package/dist/src/cli.js +8 -5
  8. package/dist/src/cli.js.map +1 -1
  9. package/dist/src/client.d.ts +34 -7
  10. package/dist/src/client.d.ts.map +1 -1
  11. package/dist/src/client.js +224 -52
  12. package/dist/src/client.js.map +1 -1
  13. package/dist/src/commands/agent-browser.d.ts +3 -0
  14. package/dist/src/commands/agent-browser.d.ts.map +1 -0
  15. package/dist/src/commands/agent-browser.js +27 -0
  16. package/dist/src/commands/agent-browser.js.map +1 -0
  17. package/dist/src/commands/auth.d.ts +1 -1
  18. package/dist/src/commands/auth.d.ts.map +1 -1
  19. package/dist/src/commands/auth.js +4 -2
  20. package/dist/src/commands/auth.js.map +1 -1
  21. package/dist/src/commands/dashboard.d.ts +13 -0
  22. package/dist/src/commands/dashboard.d.ts.map +1 -1
  23. package/dist/src/commands/dashboard.js +10 -18
  24. package/dist/src/commands/dashboard.js.map +1 -1
  25. package/dist/src/commands/helpers.d.ts +1 -1
  26. package/dist/src/commands/helpers.d.ts.map +1 -1
  27. package/dist/src/commands/helpers.js +2 -2
  28. package/dist/src/commands/helpers.js.map +1 -1
  29. package/dist/src/commands/index.d.ts +3 -1
  30. package/dist/src/commands/index.d.ts.map +1 -1
  31. package/dist/src/commands/index.js +3 -1
  32. package/dist/src/commands/index.js.map +1 -1
  33. package/dist/src/commands/mentoring.d.ts.map +1 -1
  34. package/dist/src/commands/mentoring.js +54 -29
  35. package/dist/src/commands/mentoring.js.map +1 -1
  36. package/dist/src/commands/notice.d.ts.map +1 -1
  37. package/dist/src/commands/notice.js +2 -1
  38. package/dist/src/commands/notice.js.map +1 -1
  39. package/dist/src/commands/report.d.ts.map +1 -1
  40. package/dist/src/commands/report.js +4 -2
  41. package/dist/src/commands/report.js.map +1 -1
  42. package/dist/src/commands/room.d.ts.map +1 -1
  43. package/dist/src/commands/room.js +125 -2
  44. package/dist/src/commands/room.js.map +1 -1
  45. package/dist/src/commands/schedule.d.ts +3 -0
  46. package/dist/src/commands/schedule.d.ts.map +1 -0
  47. package/dist/src/commands/schedule.js +27 -0
  48. package/dist/src/commands/schedule.js.map +1 -0
  49. package/dist/src/commands/team.d.ts.map +1 -1
  50. package/dist/src/commands/team.js +55 -4
  51. package/dist/src/commands/team.js.map +1 -1
  52. package/dist/src/commands/toz.d.ts +16 -0
  53. package/dist/src/commands/toz.d.ts.map +1 -0
  54. package/dist/src/commands/toz.js +488 -0
  55. package/dist/src/commands/toz.js.map +1 -0
  56. package/dist/src/constants.d.ts +5 -5
  57. package/dist/src/constants.d.ts.map +1 -1
  58. package/dist/src/constants.js +20 -8
  59. package/dist/src/constants.js.map +1 -1
  60. package/dist/src/credential-manager.d.ts +15 -0
  61. package/dist/src/credential-manager.d.ts.map +1 -1
  62. package/dist/src/credential-manager.js +46 -0
  63. package/dist/src/credential-manager.js.map +1 -1
  64. package/dist/src/formatters.d.ts +11 -3
  65. package/dist/src/formatters.d.ts.map +1 -1
  66. package/dist/src/formatters.js +281 -52
  67. package/dist/src/formatters.js.map +1 -1
  68. package/dist/src/http.d.ts +8 -0
  69. package/dist/src/http.d.ts.map +1 -1
  70. package/dist/src/http.js +29 -1
  71. package/dist/src/http.js.map +1 -1
  72. package/dist/src/index.d.ts +8 -1
  73. package/dist/src/index.d.ts.map +1 -1
  74. package/dist/src/index.js +4 -1
  75. package/dist/src/index.js.map +1 -1
  76. package/dist/src/session-recovery.js +2 -0
  77. package/dist/src/session-recovery.js.map +1 -1
  78. package/dist/src/shared/utils/swmaestro.d.ts +34 -1
  79. package/dist/src/shared/utils/swmaestro.d.ts.map +1 -1
  80. package/dist/src/shared/utils/swmaestro.js +102 -39
  81. package/dist/src/shared/utils/swmaestro.js.map +1 -1
  82. package/dist/src/shared/utils/team-action-params.d.ts +3 -0
  83. package/dist/src/shared/utils/team-action-params.d.ts.map +1 -0
  84. package/dist/src/shared/utils/team-action-params.js +10 -0
  85. package/dist/src/shared/utils/team-action-params.js.map +1 -0
  86. package/dist/src/shared/utils/team-params.d.ts +12 -0
  87. package/dist/src/shared/utils/team-params.d.ts.map +1 -0
  88. package/dist/src/shared/utils/team-params.js +38 -0
  89. package/dist/src/shared/utils/team-params.js.map +1 -0
  90. package/dist/src/toz-client.d.ts +89 -0
  91. package/dist/src/toz-client.d.ts.map +1 -0
  92. package/dist/src/toz-client.js +204 -0
  93. package/dist/src/toz-client.js.map +1 -0
  94. package/dist/src/toz-pending-store.d.ts +30 -0
  95. package/dist/src/toz-pending-store.d.ts.map +1 -0
  96. package/dist/src/toz-pending-store.js +36 -0
  97. package/dist/src/toz-pending-store.js.map +1 -0
  98. package/dist/src/types.d.ts +147 -10
  99. package/dist/src/types.d.ts.map +1 -1
  100. package/dist/src/types.js +74 -6
  101. package/dist/src/types.js.map +1 -1
  102. package/package.json +5 -1
  103. package/src/agent-browser-launcher.test.ts +263 -0
  104. package/src/agent-browser-launcher.ts +159 -0
  105. package/src/cli.ts +10 -5
  106. package/src/client.test.ts +673 -30
  107. package/src/client.ts +287 -67
  108. package/src/commands/agent-browser.ts +33 -0
  109. package/src/commands/auth.test.ts +77 -26
  110. package/src/commands/auth.ts +5 -3
  111. package/src/commands/dashboard.test.ts +57 -0
  112. package/src/commands/dashboard.ts +22 -19
  113. package/src/commands/helpers.test.ts +76 -25
  114. package/src/commands/helpers.ts +3 -3
  115. package/src/commands/index.ts +3 -1
  116. package/src/commands/mentoring.ts +60 -29
  117. package/src/commands/notice.ts +2 -1
  118. package/src/commands/report.ts +4 -2
  119. package/src/commands/room.ts +160 -1
  120. package/src/commands/schedule.ts +32 -0
  121. package/src/commands/team.ts +73 -5
  122. package/src/commands/toz.test.ts +51 -0
  123. package/src/commands/toz.ts +607 -0
  124. package/src/constants.ts +20 -8
  125. package/src/credential-manager.test.ts +98 -0
  126. package/src/credential-manager.ts +50 -0
  127. package/src/formatters.test.ts +528 -33
  128. package/src/formatters.ts +309 -55
  129. package/src/http.test.ts +71 -2
  130. package/src/http.ts +41 -2
  131. package/src/index.ts +23 -1
  132. package/src/session-recovery.ts +2 -0
  133. package/src/shared/utils/swmaestro.test.ts +245 -9
  134. package/src/shared/utils/swmaestro.ts +150 -47
  135. package/src/shared/utils/team-action-params.test.ts +32 -0
  136. package/src/shared/utils/team-action-params.ts +10 -0
  137. package/src/shared/utils/team-params.test.ts +141 -0
  138. package/src/shared/utils/team-params.ts +53 -0
  139. package/src/toz-client.test.ts +243 -0
  140. package/src/toz-client.ts +311 -0
  141. package/src/toz-pending-store.test.ts +91 -0
  142. package/src/toz-pending-store.ts +62 -0
  143. package/src/types.test.ts +26 -13
  144. package/src/types.ts +87 -7
  145. package/dist/src/commands/event.d.ts +0 -3
  146. package/dist/src/commands/event.d.ts.map +0 -1
  147. package/dist/src/commands/event.js +0 -58
  148. package/dist/src/commands/event.js.map +0 -1
  149. package/src/commands/event.ts +0 -73
@@ -0,0 +1,607 @@
1
+ import { stdin as input, stdout as output } from 'node:process'
2
+ import { createInterface } from 'node:readline/promises'
3
+
4
+ import { Command } from 'commander'
5
+
6
+ import { TOZ_BRANCHES } from '../constants'
7
+ import { CredentialManager } from '../credential-manager'
8
+ import { handleError } from '../shared/utils/error-handler'
9
+ import { formatOutput } from '../shared/utils/output'
10
+ import * as stderr from '../shared/utils/stderr'
11
+ import { formatPhone, parsePhone } from '../shared/utils/toz'
12
+ import { TozClient } from '../toz-client'
13
+ import type { TozPendingReservation } from '../toz-pending-store'
14
+ import { TozPendingStore } from '../toz-pending-store'
15
+
16
+ type LoginOpts = { name: string; phone: string; pretty?: boolean }
17
+ type StatusOpts = { pretty?: boolean }
18
+ type BranchesOpts = { pretty?: boolean }
19
+ type MeetingsOpts = { pretty?: boolean }
20
+ type AvailableOpts = {
21
+ date: string
22
+ start: string
23
+ duration: string
24
+ userCount: string
25
+ branch?: string[]
26
+ pretty?: boolean
27
+ }
28
+ type CheckOpts = {
29
+ date: string
30
+ time: string[]
31
+ duration: string
32
+ userCount: string
33
+ branch?: string[]
34
+ pretty?: boolean
35
+ }
36
+ type ReserveRequestOpts = {
37
+ date: string
38
+ start: string
39
+ duration: string
40
+ userCount: string
41
+ boothId: string
42
+ meetingId?: string
43
+ newMeeting?: string
44
+ email: string
45
+ memo?: string
46
+ name?: string
47
+ phone?: string
48
+ pretty?: boolean
49
+ }
50
+ type ReserveConfirmOpts = { pin?: string; pretty?: boolean }
51
+ type ReserveOpts = ReserveRequestOpts & { pin?: string }
52
+ type ListOpts = {
53
+ start?: string
54
+ end?: string
55
+ meeting?: string
56
+ name?: string
57
+ phone?: string
58
+ pretty?: boolean
59
+ }
60
+ type CancelOpts = { name?: string; phone?: string; pretty?: boolean }
61
+ type LogoutOpts = { pretty?: boolean }
62
+ type TozIdentityStore = Pick<CredentialManager, 'getTozIdentity'>
63
+ type TozIdentityPrompt = () => Promise<string>
64
+ type TozPinPrompt = () => Promise<string>
65
+
66
+ function parseStrictPositiveInt(value: string, flag: string): number {
67
+ if (!/^\d+$/.test(value)) throw new Error(`Invalid --${flag}: ${value} (expected positive integer)`)
68
+ const n = Number.parseInt(value, 10)
69
+ if (!Number.isSafeInteger(n) || n <= 0) throw new Error(`Invalid --${flag}: ${value} (expected positive integer)`)
70
+ return n
71
+ }
72
+
73
+ function parseDurationFlag(value: string): number {
74
+ const match = /^(\d+)\s*([hm])?$/i.exec(value.trim())
75
+ if (!match) throw new Error(`Invalid --duration: ${value} (use e.g. "2h", "120", "150m")`)
76
+ const n = Number.parseInt(match[1], 10)
77
+ const unit = (match[2] ?? 'm').toLowerCase()
78
+ return unit === 'h' ? n * 60 : n
79
+ }
80
+
81
+ function parseUserCount(value: string): number {
82
+ const n = Number.parseInt(value, 10)
83
+ if (!Number.isFinite(n) || n <= 0) throw new Error(`Invalid --user-count: ${value}`)
84
+ return n
85
+ }
86
+
87
+ function resolveBranchIds(values: readonly string[] | undefined): number[] {
88
+ if (!values || values.length === 0) return TOZ_BRANCHES.map((b) => b.id)
89
+ const ids: number[] = []
90
+ for (const value of values) {
91
+ const numeric = Number.parseInt(value, 10)
92
+ if (Number.isFinite(numeric)) {
93
+ ids.push(numeric)
94
+ continue
95
+ }
96
+ const matched = TOZ_BRANCHES.find((b) => b.name === value || b.name.includes(value))
97
+ if (!matched) throw new Error(`Unknown branch: ${value}`)
98
+ ids.push(matched.id)
99
+ }
100
+ return ids
101
+ }
102
+
103
+ export async function resolveTozIdentity(
104
+ flagName: string | undefined,
105
+ flagPhone: string | undefined,
106
+ options: { promptPhone?: TozIdentityPrompt; store?: TozIdentityStore } = {},
107
+ ): Promise<{ name: string; phone: string }> {
108
+ if (flagName && flagPhone) return { name: flagName, phone: flagPhone }
109
+ const store = options.store ?? new CredentialManager()
110
+ const stored = await store.getTozIdentity()
111
+ const name = flagName ?? stored?.name
112
+ let phone = flagPhone ?? stored?.phone
113
+ if (!name) {
114
+ throw new Error('Toz identity not set. Run: opensoma toz login --name <name> --phone <phone>')
115
+ }
116
+ if (!phone) {
117
+ if (!options.promptPhone) {
118
+ throw new Error('Toz identity not set. Run: opensoma toz login --name <name> --phone <phone>')
119
+ }
120
+ phone = await options.promptPhone()
121
+ }
122
+ return { name, phone }
123
+ }
124
+
125
+ export async function resolveTozPin(pin: string | undefined, promptPin: TozPinPrompt): Promise<string> {
126
+ return pin ?? (await promptPin())
127
+ }
128
+
129
+ async function loginAction(options: LoginOpts): Promise<void> {
130
+ try {
131
+ const manager = new CredentialManager()
132
+ await manager.setTozIdentity(options.name, options.phone)
133
+ console.log(
134
+ formatOutput(
135
+ { ok: true, message: `Toz identity saved for ${options.name} (${maskPhone(options.phone)})` },
136
+ options.pretty,
137
+ ),
138
+ )
139
+ } catch (error) {
140
+ handleError(error)
141
+ }
142
+ }
143
+
144
+ async function logoutAction(options: LogoutOpts): Promise<void> {
145
+ try {
146
+ await new CredentialManager().clearTozIdentity()
147
+ await new TozPendingStore().clear()
148
+ console.log(formatOutput({ ok: true }, options.pretty))
149
+ } catch (error) {
150
+ handleError(error)
151
+ }
152
+ }
153
+
154
+ async function statusAction(options: StatusOpts): Promise<void> {
155
+ try {
156
+ const stored = await new CredentialManager().getTozIdentity()
157
+ if (!stored) {
158
+ console.log(formatOutput({ loggedIn: false }, options.pretty))
159
+ return
160
+ }
161
+ console.log(formatOutput({ loggedIn: true, name: stored.name, phone: maskPhone(stored.phone) }, options.pretty))
162
+ } catch (error) {
163
+ handleError(error)
164
+ }
165
+ }
166
+
167
+ async function branchesAction(options: BranchesOpts): Promise<void> {
168
+ try {
169
+ const branches = await new TozClient().branches()
170
+ console.log(formatOutput(branches, options.pretty))
171
+ } catch (error) {
172
+ handleError(error)
173
+ }
174
+ }
175
+
176
+ async function meetingsAction(options: MeetingsOpts): Promise<void> {
177
+ try {
178
+ const meetings = await new TozClient().meetings()
179
+ console.log(formatOutput(meetings, options.pretty))
180
+ } catch (error) {
181
+ handleError(error)
182
+ }
183
+ }
184
+
185
+ async function availableAction(options: AvailableOpts): Promise<void> {
186
+ try {
187
+ const booths = await new TozClient().available({
188
+ date: options.date,
189
+ startTime: options.start,
190
+ durationMinutes: parseDurationFlag(options.duration),
191
+ userCount: parseUserCount(options.userCount),
192
+ branchIds: resolveBranchIds(options.branch),
193
+ })
194
+ console.log(formatOutput(booths, options.pretty))
195
+ } catch (error) {
196
+ handleError(error)
197
+ }
198
+ }
199
+
200
+ async function checkAction(options: CheckOpts): Promise<void> {
201
+ try {
202
+ const startedAt = Date.now()
203
+ const checked = await new TozClient().check({
204
+ date: options.date,
205
+ startTimes: options.time,
206
+ durationMinutes: parseDurationFlag(options.duration),
207
+ userCount: parseUserCount(options.userCount),
208
+ branchIds: resolveBranchIds(options.branch),
209
+ })
210
+ console.log(
211
+ formatOutput(
212
+ {
213
+ date: options.date,
214
+ durationMinutes: parseDurationFlag(options.duration),
215
+ userCount: parseUserCount(options.userCount),
216
+ checked,
217
+ requestsIssued: options.time.length,
218
+ wallTimeMs: Date.now() - startedAt,
219
+ },
220
+ options.pretty,
221
+ ),
222
+ )
223
+ } catch (error) {
224
+ handleError(error)
225
+ }
226
+ }
227
+
228
+ async function reserveRequestAction(options: ReserveRequestOpts): Promise<void> {
229
+ try {
230
+ const identity = await resolveTozIdentity(options.name, options.phone, { promptPhone })
231
+ const durationMinutes = parseDurationFlag(options.duration)
232
+ const userCount = parseUserCount(options.userCount)
233
+ const boothId = parseStrictPositiveInt(options.boothId, 'booth-id')
234
+ const meetingId = options.meetingId ? parseStrictPositiveInt(options.meetingId, 'meeting-id') : undefined
235
+ if (!meetingId && !options.newMeeting) throw new Error('--meeting-id or --new-meeting is required')
236
+
237
+ const client = new TozClient()
238
+ const reserved = await client.reserveBooth({
239
+ date: options.date,
240
+ startTime: options.start,
241
+ durationMinutes,
242
+ userCount,
243
+ boothId,
244
+ })
245
+ await client.skipEquipment({
246
+ reservationId: reserved.reservationId,
247
+ date: options.date,
248
+ startTime: options.start,
249
+ durationMinutes,
250
+ })
251
+ await client.sendOtp(identity.phone)
252
+
253
+ const endTime = computeEndTime(options.start, durationMinutes)
254
+ const now = new Date()
255
+ const pending: TozPendingReservation = {
256
+ reservationId: reserved.reservationId,
257
+ cookies: client.http.getCookies(),
258
+ branchName: reserved.branchName,
259
+ branchTel: reserved.branchTel,
260
+ boothGroupName: reserved.boothGroupName,
261
+ isLargeBooth: reserved.isLargeBooth,
262
+ date: options.date,
263
+ startTime: options.start,
264
+ endTime,
265
+ durationMinutes,
266
+ userCount,
267
+ boothId,
268
+ meetingId,
269
+ newMeetingName: options.newMeeting,
270
+ email: options.email,
271
+ memo: options.memo,
272
+ name: identity.name,
273
+ phone: identity.phone,
274
+ createdAt: now.toISOString(),
275
+ expiresAt: new Date(now.getTime() + 5 * 60 * 1000).toISOString(),
276
+ }
277
+ await new TozPendingStore().set(pending)
278
+
279
+ console.log(
280
+ formatOutput(
281
+ {
282
+ ok: true,
283
+ reservationId: reserved.reservationId,
284
+ branchName: reserved.branchName,
285
+ boothGroupName: reserved.boothGroupName,
286
+ date: options.date,
287
+ startTime: options.start,
288
+ endTime,
289
+ expiresAt: pending.expiresAt,
290
+ message: `SMS sent to ${maskPhone(identity.phone)}. Run: opensoma toz reserve-confirm --pin <pin>`,
291
+ },
292
+ options.pretty,
293
+ ),
294
+ )
295
+ } catch (error) {
296
+ handleError(error)
297
+ }
298
+ }
299
+
300
+ async function reserveConfirmAction(options: ReserveConfirmOpts): Promise<void> {
301
+ try {
302
+ const store = new TozPendingStore()
303
+ const pending = await store.get()
304
+ if (!pending) {
305
+ throw new Error('No pending toz reservation. Run: opensoma toz reserve-request first.')
306
+ }
307
+ if (Date.now() > new Date(pending.expiresAt).getTime()) {
308
+ await store.clear()
309
+ throw new Error(
310
+ 'Pending toz reservation expired (5-minute hold lapsed). Run: opensoma toz reserve-request again.',
311
+ )
312
+ }
313
+
314
+ const pin = await resolveTozPin(options.pin, promptPin)
315
+ const client = new TozClient({ cookies: pending.cookies })
316
+ const result = await client.confirm({
317
+ reservationId: pending.reservationId,
318
+ date: pending.date,
319
+ startTime: pending.startTime,
320
+ durationMinutes: pending.durationMinutes,
321
+ name: pending.name,
322
+ phone: pending.phone,
323
+ email: pending.email,
324
+ pinNum: pin,
325
+ meetingId: pending.meetingId,
326
+ newMeetingName: pending.newMeetingName,
327
+ memo: pending.memo,
328
+ })
329
+
330
+ await store.clear()
331
+ console.log(
332
+ formatOutput(
333
+ {
334
+ ok: true,
335
+ reservationId: pending.reservationId,
336
+ branchName: pending.branchName,
337
+ boothGroupName: pending.boothGroupName,
338
+ date: pending.date,
339
+ startTime: pending.startTime,
340
+ endTime: pending.endTime,
341
+ message: result.resultMsg,
342
+ },
343
+ options.pretty,
344
+ ),
345
+ )
346
+ } catch (error) {
347
+ handleError(error)
348
+ }
349
+ }
350
+
351
+ async function reserveAction(options: ReserveOpts): Promise<void> {
352
+ try {
353
+ const identity = await resolveTozIdentity(options.name, options.phone, { promptPhone })
354
+ const durationMinutes = parseDurationFlag(options.duration)
355
+ const userCount = parseUserCount(options.userCount)
356
+ const boothId = parseStrictPositiveInt(options.boothId, 'booth-id')
357
+ const meetingId = options.meetingId ? parseStrictPositiveInt(options.meetingId, 'meeting-id') : undefined
358
+ if (!meetingId && !options.newMeeting) throw new Error('--meeting-id or --new-meeting is required')
359
+
360
+ const client = new TozClient()
361
+ stderr.info(`[1/4] Reserving booth ${boothId}...`)
362
+ const reserved = await client.reserveBooth({
363
+ date: options.date,
364
+ startTime: options.start,
365
+ durationMinutes,
366
+ userCount,
367
+ boothId,
368
+ })
369
+ stderr.info(` ok (reservationId=${reserved.reservationId}, ${reserved.branchName} ${reserved.boothGroupName})`)
370
+
371
+ stderr.info('[2/4] Skipping equipment...')
372
+ await client.skipEquipment({
373
+ reservationId: reserved.reservationId,
374
+ date: options.date,
375
+ startTime: options.start,
376
+ durationMinutes,
377
+ })
378
+ stderr.info(' ok')
379
+
380
+ stderr.info(`[3/4] Sending SMS to ${maskPhone(identity.phone)}...`)
381
+ await client.sendOtp(identity.phone)
382
+ stderr.info(' ok')
383
+
384
+ const pin = options.pin ?? (await promptPin())
385
+ stderr.info('[4/4] Confirming reservation...')
386
+ const result = await client.confirm({
387
+ reservationId: reserved.reservationId,
388
+ date: options.date,
389
+ startTime: options.start,
390
+ durationMinutes,
391
+ name: identity.name,
392
+ phone: identity.phone,
393
+ email: options.email,
394
+ pinNum: pin,
395
+ meetingId,
396
+ newMeetingName: options.newMeeting,
397
+ memo: options.memo,
398
+ })
399
+ stderr.info(' ok')
400
+
401
+ console.log(
402
+ formatOutput(
403
+ {
404
+ ok: true,
405
+ reservationId: reserved.reservationId,
406
+ branchName: reserved.branchName,
407
+ boothGroupName: reserved.boothGroupName,
408
+ date: options.date,
409
+ startTime: options.start,
410
+ endTime: computeEndTime(options.start, durationMinutes),
411
+ message: result.resultMsg,
412
+ },
413
+ options.pretty,
414
+ ),
415
+ )
416
+ } catch (error) {
417
+ handleError(error)
418
+ }
419
+ }
420
+
421
+ async function listAction(options: ListOpts): Promise<void> {
422
+ try {
423
+ const identity = await resolveTozIdentity(options.name, options.phone)
424
+ const reservations = await new TozClient().myReservations({
425
+ name: identity.name,
426
+ phone: identity.phone,
427
+ startDate: options.start,
428
+ endDate: options.end,
429
+ meetingName: options.meeting,
430
+ })
431
+ console.log(formatOutput(reservations, options.pretty))
432
+ } catch (error) {
433
+ handleError(error)
434
+ }
435
+ }
436
+
437
+ async function cancelAction(reservationId: string, options: CancelOpts): Promise<void> {
438
+ try {
439
+ const id = parseStrictPositiveInt(reservationId, 'reservationId')
440
+ const identity = await resolveTozIdentity(options.name, options.phone)
441
+ await new TozClient().cancel({ reservationId: id, name: identity.name, phone: identity.phone })
442
+ console.log(formatOutput({ ok: true, message: '취소되었습니다.' }, options.pretty))
443
+ } catch (error) {
444
+ handleError(error)
445
+ }
446
+ }
447
+
448
+ function maskPhone(phone: string): string {
449
+ return phone.replace(
450
+ /^(\d{3})-?(\d{3,4})-?(\d{4})$/,
451
+ (_, a: string, _b: string, c: string) => `${a}-${'*'.repeat(_b.length)}-${c}`,
452
+ )
453
+ }
454
+
455
+ function computeEndTime(start: string, durationMinutes: number): string {
456
+ const [h, m] = start.split(':').map((s) => Number.parseInt(s, 10))
457
+ const total = h * 60 + m + durationMinutes
458
+ const eh = Math.floor(total / 60) % 24
459
+ const em = total % 60
460
+ return `${String(eh).padStart(2, '0')}:${String(em).padStart(2, '0')}`
461
+ }
462
+
463
+ async function promptPin(): Promise<string> {
464
+ const rl = createInterface({ input, output })
465
+ try {
466
+ const answer = await rl.question('Enter 6-digit PIN from SMS: ')
467
+ return answer.trim()
468
+ } finally {
469
+ rl.close()
470
+ }
471
+ }
472
+
473
+ async function promptPhone(): Promise<string> {
474
+ const rl = createInterface({ input, output })
475
+ try {
476
+ while (true) {
477
+ const answer = await rl.question('Phone number (e.g. 010-1234-5678): ')
478
+ const phone = answer.trim()
479
+ try {
480
+ return formatPhone(parsePhone(phone))
481
+ } catch (error) {
482
+ stderr.warn(error instanceof Error ? error.message : String(error))
483
+ }
484
+ }
485
+ } finally {
486
+ rl.close()
487
+ }
488
+ }
489
+
490
+ export const tozCommand = new Command('toz')
491
+ .description('Manage TOZ external meeting room reservations (SW마에스트로 partnership)')
492
+ .addCommand(
493
+ new Command('login')
494
+ .description('Save TOZ identity (name + phone) to credentials')
495
+ .requiredOption('--name <name>', 'Reservation holder name')
496
+ .requiredOption('--phone <phone>', 'Phone number, e.g. 010-1234-5678')
497
+ .option('--pretty', 'Pretty print JSON output')
498
+ .action(loginAction),
499
+ )
500
+ .addCommand(
501
+ new Command('logout')
502
+ .description('Clear stored TOZ identity and pending reservation state')
503
+ .option('--pretty', 'Pretty print JSON output')
504
+ .action(logoutAction),
505
+ )
506
+ .addCommand(
507
+ new Command('status')
508
+ .description('Show stored TOZ identity')
509
+ .option('--pretty', 'Pretty print JSON output')
510
+ .action(statusAction),
511
+ )
512
+ .addCommand(
513
+ new Command('branches')
514
+ .description('List partner branches (live from booking.htm)')
515
+ .option('--pretty', 'Pretty print JSON output')
516
+ .action(branchesAction),
517
+ )
518
+ .addCommand(
519
+ new Command('meetings')
520
+ .description('List active SW마에스트로 meetings (live from booking.htm)')
521
+ .option('--pretty', 'Pretty print JSON output')
522
+ .action(meetingsAction),
523
+ )
524
+ .addCommand(
525
+ new Command('available')
526
+ .description('Show available booths for one date+time+duration')
527
+ .requiredOption('--date <date>', 'YYYY-MM-DD')
528
+ .requiredOption('--start <time>', 'HH:MM')
529
+ .requiredOption('--duration <duration>', 'e.g. 2h, 150m, 120 (default minutes)')
530
+ .requiredOption('--user-count <n>', 'Number of attendees')
531
+ .option('--branch <id-or-name...>', 'Branch IDs or names (default: all 9)')
532
+ .option('--pretty', 'Pretty print JSON output')
533
+ .action(availableAction),
534
+ )
535
+ .addCommand(
536
+ new Command('check')
537
+ .description('Check booth availability for multiple times (max 6, serial)')
538
+ .requiredOption('--date <date>', 'YYYY-MM-DD')
539
+ .requiredOption('--time <time...>', 'HH:MM (repeatable, max 6)')
540
+ .requiredOption('--duration <duration>', 'e.g. 2h')
541
+ .requiredOption('--user-count <n>', 'Number of attendees')
542
+ .option('--branch <id-or-name...>', 'Branch IDs or names')
543
+ .option('--pretty', 'Pretty print JSON output')
544
+ .action(checkAction),
545
+ )
546
+ .addCommand(
547
+ new Command('reserve-request')
548
+ .description('Step 1 of non-interactive reservation: hold booth + send SMS')
549
+ .requiredOption('--date <date>', 'YYYY-MM-DD')
550
+ .requiredOption('--start <time>', 'HH:MM')
551
+ .requiredOption('--duration <duration>', 'e.g. 2h')
552
+ .requiredOption('--user-count <n>', 'Number of attendees')
553
+ .requiredOption('--booth-id <id>', 'Booth ID from `toz available`')
554
+ .option('--meeting-id <id>', 'Meeting ID from `toz meetings`')
555
+ .option('--new-meeting <name>', 'Create a new meeting with this name')
556
+ .requiredOption('--email <email>', 'Contact email')
557
+ .option('--memo <text>', 'Reservation memo')
558
+ .option('--name <name>', 'Override stored name')
559
+ .option('--phone <phone>', 'Override stored phone')
560
+ .option('--pretty', 'Pretty print JSON output')
561
+ .action(reserveRequestAction),
562
+ )
563
+ .addCommand(
564
+ new Command('reserve-confirm')
565
+ .description('Step 2 of non-interactive reservation: finalize with PIN')
566
+ .option('--pin <pin>', '6-digit PIN from SMS (prompts if omitted)')
567
+ .option('--pretty', 'Pretty print JSON output')
568
+ .action(reserveConfirmAction),
569
+ )
570
+ .addCommand(
571
+ new Command('reserve')
572
+ .description('Interactive single-process reservation (prompts for PIN)')
573
+ .requiredOption('--date <date>', 'YYYY-MM-DD')
574
+ .requiredOption('--start <time>', 'HH:MM')
575
+ .requiredOption('--duration <duration>', 'e.g. 2h')
576
+ .requiredOption('--user-count <n>', 'Number of attendees')
577
+ .requiredOption('--booth-id <id>', 'Booth ID')
578
+ .option('--meeting-id <id>', 'Meeting ID')
579
+ .option('--new-meeting <name>', 'Create new meeting with this name')
580
+ .requiredOption('--email <email>', 'Contact email')
581
+ .option('--memo <text>', 'Reservation memo')
582
+ .option('--name <name>', 'Override stored name')
583
+ .option('--phone <phone>', 'Override stored phone')
584
+ .option('--pin <pin>', 'PIN if already received (skip prompt)')
585
+ .option('--pretty', 'Pretty print JSON output')
586
+ .action(reserveAction),
587
+ )
588
+ .addCommand(
589
+ new Command('list')
590
+ .description('List my TOZ reservations')
591
+ .option('--start <date>', 'YYYY-MM-DD')
592
+ .option('--end <date>', 'YYYY-MM-DD')
593
+ .option('--meeting <name>', 'Filter by meeting name')
594
+ .option('--name <name>', 'Override stored name')
595
+ .option('--phone <phone>', 'Override stored phone')
596
+ .option('--pretty', 'Pretty print JSON output')
597
+ .action(listAction),
598
+ )
599
+ .addCommand(
600
+ new Command('cancel')
601
+ .description('Cancel a TOZ reservation')
602
+ .argument('<reservationId>')
603
+ .option('--name <name>', 'Override stored name')
604
+ .option('--phone <phone>', 'Override stored phone')
605
+ .option('--pretty', 'Pretty print JSON output')
606
+ .action(cancelAction),
607
+ )
package/src/constants.ts CHANGED
@@ -5,8 +5,8 @@ export const MENU_NO = {
5
5
  DASHBOARD: '200026',
6
6
  NOTICE: '200038',
7
7
  TEAM: '200093',
8
+ SCHEDULE: '200043',
8
9
  MENTORING: '200046',
9
- EVENT: '200045',
10
10
  APPLICATION_HISTORY: '200047',
11
11
  ROOM: '200058',
12
12
  MEMBER_INFO: '200036',
@@ -25,16 +25,20 @@ export const ROOM_IDS: Record<string, number> = {
25
25
  A8: 24,
26
26
  }
27
27
 
28
+ // Values mirror the native <select name="place"> options verbatim, including the
29
+ // trailing spaces on 건대/역삼/홍대 that swmaestro.ai ships in its HTML. Per the
30
+ // mirror rule in AGENTS.md, we send the native bytes exactly, not a "cleaned up"
31
+ // variant. If a future native release drops the spaces, update these here.
28
32
  export const VENUES = {
29
33
  TOZ_GWANGHWAMUN: '토즈-광화문점',
30
34
  TOZ_YANGJAE: '토즈-양재점',
31
35
  TOZ_GANGNAM_CONFERENCE_CENTER: '토즈-강남컨퍼런스센터점',
32
- TOZ_KONKUK: '토즈-건대점',
36
+ TOZ_KONKUK: '토즈-건대점 ',
33
37
  TOZ_GANGNAM_TOWER: '토즈-강남역토즈타워점',
34
38
  TOZ_SEOLLEUNG: '토즈-선릉점',
35
- TOZ_YEOKSAM: '토즈-역삼점',
36
- TOZ_HONGDAE: '토즈-홍대점',
37
- TOZ_SINCHON_BUSINESS_CENTER: '토즈-신촌비즈니스센터점',
39
+ TOZ_YEOKSAM: '토즈-역삼점 ',
40
+ TOZ_HONGDAE: '토즈-홍대점 ',
41
+ TOZ_SINCHON_BUSINESS_CENTER: '연수센터-7',
38
42
  ONLINE_WEBEX: '온라인(Webex)',
39
43
  SPACE_A1: '스페이스 A1',
40
44
  SPACE_A2: '스페이스 A2',
@@ -55,12 +59,20 @@ export const VENUE_ALIASES: Record<string, string> = {
55
59
  광화문점: '토즈-광화문점',
56
60
  양재점: '토즈-양재점',
57
61
  강남컨퍼런스센터점: '토즈-강남컨퍼런스센터점',
58
- 건대점: '토즈-건대점',
62
+ 건대점: '토즈-건대점 ',
59
63
  강남역토즈타워점: '토즈-강남역토즈타워점',
60
64
  선릉점: '토즈-선릉점',
61
- 역삼점: '토즈-역삼점',
62
- 홍대점: '토즈-홍대점',
65
+ 역삼점: '토즈-역삼점 ',
66
+ 홍대점: '토즈-홍대점 ',
63
67
  신촌비즈니스센터점: '연수센터-7',
68
+ '토즈-광화문점': '토즈-광화문점',
69
+ '토즈-양재점': '토즈-양재점',
70
+ '토즈-강남컨퍼런스센터점': '토즈-강남컨퍼런스센터점',
71
+ '토즈-건대점': '토즈-건대점 ',
72
+ '토즈-강남역토즈타워점': '토즈-강남역토즈타워점',
73
+ '토즈-선릉점': '토즈-선릉점',
74
+ '토즈-역삼점': '토즈-역삼점 ',
75
+ '토즈-홍대점': '토즈-홍대점 ',
64
76
  '토즈-신촌비즈니스센터점': '연수센터-7',
65
77
  }
66
78