huly-mcp-sdk 0.5.0 → 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.
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huly-mcp-sdk",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "MCP server for Huly
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "MCP server for Huly \u2014 connect Claude Desktop to your Huly workspace via the native WebSocket SDK",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"huly",
|
|
7
7
|
"mcp",
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Delete the ASCII art comments from the 4 epic issues (MCARE-4..7)
|
|
4
|
-
* Run: node --env-file=.env scripts/clean-comments.mjs
|
|
5
|
-
*/
|
|
6
|
-
import accountClientPkg from '@hcengineering/account-client'
|
|
7
|
-
import corePkg from '@hcengineering/core'
|
|
8
|
-
import serverClientPkg from '@hcengineering/server-client'
|
|
9
|
-
import trackerPkg from '@hcengineering/tracker'
|
|
10
|
-
import chunterPkg from '@hcengineering/chunter'
|
|
11
|
-
|
|
12
|
-
const { getClient: getRawAccountClient } = accountClientPkg
|
|
13
|
-
const { TxOperations } = corePkg
|
|
14
|
-
const { createClient } = serverClientPkg
|
|
15
|
-
const tracker = trackerPkg.default ?? trackerPkg
|
|
16
|
-
const chunter = chunterPkg.default ?? chunterPkg
|
|
17
|
-
|
|
18
|
-
const ACCOUNTS_URL = process.env.HULY_ACCOUNTS_URL ?? 'https://account.huly.app'
|
|
19
|
-
|
|
20
|
-
async function connect () {
|
|
21
|
-
const workspaceUrl = process.env.HULY_WORKSPACE
|
|
22
|
-
const hulyToken = process.env.HULY_TOKEN
|
|
23
|
-
if (!workspaceUrl || !hulyToken) throw new Error('HULY_WORKSPACE and HULY_TOKEN required')
|
|
24
|
-
|
|
25
|
-
const authedClient = getRawAccountClient(ACCOUNTS_URL, hulyToken)
|
|
26
|
-
const info = await authedClient.getLoginInfoByToken()
|
|
27
|
-
if (!info) throw new Error('Token invalid')
|
|
28
|
-
|
|
29
|
-
let socialId, endpoint, wsToken
|
|
30
|
-
|
|
31
|
-
if ('endpoint' in info && info.endpoint) {
|
|
32
|
-
socialId = info.socialId; endpoint = info.endpoint; wsToken = info.token
|
|
33
|
-
} else {
|
|
34
|
-
socialId = info.socialId
|
|
35
|
-
const wsInfo = await authedClient.selectWorkspace(workspaceUrl, 'external')
|
|
36
|
-
endpoint = wsInfo.endpoint; wsToken = wsInfo.token
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const rawConnection = await createClient(endpoint, wsToken)
|
|
40
|
-
const txClient = new TxOperations(rawConnection, socialId)
|
|
41
|
-
return { txClient, rawConnection }
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const { txClient, rawConnection } = await connect()
|
|
45
|
-
console.log('Connected.')
|
|
46
|
-
|
|
47
|
-
const EPICS = ['MCARE-4', 'MCARE-5', 'MCARE-6', 'MCARE-7']
|
|
48
|
-
|
|
49
|
-
for (const identifier of EPICS) {
|
|
50
|
-
const issue = await txClient.findOne(tracker.class.Issue, { identifier })
|
|
51
|
-
if (!issue) { console.log(`${identifier}: NOT FOUND`); continue }
|
|
52
|
-
|
|
53
|
-
const comments = await txClient.findAll(chunter.class.ChatMessage, { attachedTo: issue._id })
|
|
54
|
-
if (comments.length === 0) { console.log(`${identifier}: no comments`); continue }
|
|
55
|
-
|
|
56
|
-
for (const comment of comments) {
|
|
57
|
-
await txClient.removeCollection(
|
|
58
|
-
chunter.class.ChatMessage,
|
|
59
|
-
issue.space,
|
|
60
|
-
comment._id,
|
|
61
|
-
issue._id,
|
|
62
|
-
tracker.class.Issue,
|
|
63
|
-
'comments'
|
|
64
|
-
)
|
|
65
|
-
console.log(`${identifier}: deleted comment ${comment._id}`)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
await rawConnection.close()
|
|
70
|
-
console.log('Done.')
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Link EP1-EP4 documents to their corresponding epic issues via relations
|
|
4
|
-
* Run: node --env-file=.env scripts/link-docs-to-issues.mjs
|
|
5
|
-
*/
|
|
6
|
-
import accountClientPkg from '@hcengineering/account-client'
|
|
7
|
-
import corePkg from '@hcengineering/core'
|
|
8
|
-
import serverClientPkg from '@hcengineering/server-client'
|
|
9
|
-
import trackerPkg from '@hcengineering/tracker'
|
|
10
|
-
import documentPkg from '@hcengineering/document'
|
|
11
|
-
|
|
12
|
-
const { getClient: getRawAccountClient } = accountClientPkg
|
|
13
|
-
const { TxOperations } = corePkg
|
|
14
|
-
const { createClient } = serverClientPkg
|
|
15
|
-
const tracker = trackerPkg.default ?? trackerPkg
|
|
16
|
-
const document = documentPkg.default ?? documentPkg
|
|
17
|
-
|
|
18
|
-
const ACCOUNTS_URL = process.env.HULY_ACCOUNTS_URL ?? 'https://account.huly.app'
|
|
19
|
-
|
|
20
|
-
async function connect () {
|
|
21
|
-
const workspaceUrl = process.env.HULY_WORKSPACE
|
|
22
|
-
const hulyToken = process.env.HULY_TOKEN
|
|
23
|
-
if (!workspaceUrl || !hulyToken) throw new Error('HULY_WORKSPACE and HULY_TOKEN required')
|
|
24
|
-
|
|
25
|
-
const authedClient = getRawAccountClient(ACCOUNTS_URL, hulyToken)
|
|
26
|
-
const info = await authedClient.getLoginInfoByToken()
|
|
27
|
-
if (!info) throw new Error('Token invalid')
|
|
28
|
-
|
|
29
|
-
let socialId, endpoint, wsToken
|
|
30
|
-
if ('endpoint' in info && info.endpoint) {
|
|
31
|
-
socialId = info.socialId; endpoint = info.endpoint; wsToken = info.token
|
|
32
|
-
} else {
|
|
33
|
-
socialId = info.socialId
|
|
34
|
-
const wsInfo = await authedClient.selectWorkspace(workspaceUrl, 'external')
|
|
35
|
-
endpoint = wsInfo.endpoint; wsToken = wsInfo.token
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const rawConnection = await createClient(endpoint, wsToken)
|
|
39
|
-
const txClient = new TxOperations(rawConnection, socialId)
|
|
40
|
-
return { txClient, rawConnection }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const LINKS = [
|
|
44
|
-
{ issue: 'MCARE-4', docId: '69b9a635cf60316d6ce21a50' }, // EP1
|
|
45
|
-
{ issue: 'MCARE-5', docId: '69b9a63bcf60316d6ce21a52' }, // EP2
|
|
46
|
-
{ issue: 'MCARE-6', docId: '69b9a640cf60316d6ce21a54' }, // EP3
|
|
47
|
-
{ issue: 'MCARE-7', docId: '69b9a645cf60316d6ce21a56' }, // EP4
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
const { txClient, rawConnection } = await connect()
|
|
51
|
-
console.log('Connected.')
|
|
52
|
-
|
|
53
|
-
for (const { issue: identifier, docId } of LINKS) {
|
|
54
|
-
const issue = await txClient.findOne(tracker.class.Issue, { identifier })
|
|
55
|
-
if (!issue) { console.log(`${identifier}: NOT FOUND`); continue }
|
|
56
|
-
|
|
57
|
-
const doc = await txClient.findOne(document.class.Document, { _id: docId })
|
|
58
|
-
if (!doc) { console.log(`${identifier}: document ${docId} NOT FOUND`); continue }
|
|
59
|
-
|
|
60
|
-
// Build updated relations array (keep existing + add the document link)
|
|
61
|
-
const existing = issue.relations ?? []
|
|
62
|
-
const alreadyLinked = existing.some(r => r._id === docId)
|
|
63
|
-
if (alreadyLinked) { console.log(`${identifier}: already linked`); continue }
|
|
64
|
-
|
|
65
|
-
await txClient.updateDoc(tracker.class.Issue, issue.space, issue._id, {
|
|
66
|
-
relations: [...existing, { _id: doc._id, _class: doc._class }]
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
console.log(`${identifier} ↔ "${doc.title}" ✅`)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
await rawConnection.close()
|
|
73
|
-
console.log('Done.')
|
|
@@ -1,505 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* One-off script: populate the 4 medics-care epic documents with Mermaid diagrams
|
|
4
|
-
* Run: node --env-file=.env scripts/populate-docs.mjs
|
|
5
|
-
*/
|
|
6
|
-
import accountClientPkg from '@hcengineering/account-client'
|
|
7
|
-
import corePkg from '@hcengineering/core'
|
|
8
|
-
import serverClientPkg from '@hcengineering/server-client'
|
|
9
|
-
import documentPkg from '@hcengineering/document'
|
|
10
|
-
|
|
11
|
-
const { getClient: getRawAccountClient } = accountClientPkg
|
|
12
|
-
const { TxOperations } = corePkg
|
|
13
|
-
const { createClient } = serverClientPkg
|
|
14
|
-
const document = documentPkg.default ?? documentPkg
|
|
15
|
-
|
|
16
|
-
const DATALAKE_URL = 'https://dl-eu.huly.app'
|
|
17
|
-
const ACCOUNTS_URL = process.env.HULY_ACCOUNTS_URL ?? 'https://account.huly.app'
|
|
18
|
-
|
|
19
|
-
// ── Connect ───────────────────────────────────────────────────────────────────
|
|
20
|
-
|
|
21
|
-
async function connect () {
|
|
22
|
-
const workspaceUrl = process.env.HULY_WORKSPACE
|
|
23
|
-
const hulyToken = process.env.HULY_TOKEN
|
|
24
|
-
|
|
25
|
-
if (!workspaceUrl || !hulyToken) throw new Error('HULY_WORKSPACE and HULY_TOKEN are required')
|
|
26
|
-
|
|
27
|
-
const authedClient = getRawAccountClient(ACCOUNTS_URL, hulyToken)
|
|
28
|
-
const info = await authedClient.getLoginInfoByToken()
|
|
29
|
-
if (!info) throw new Error('HULY_TOKEN invalid or expired')
|
|
30
|
-
|
|
31
|
-
let socialId, endpoint, wsToken, workspaceUuid
|
|
32
|
-
|
|
33
|
-
if ('endpoint' in info && info.endpoint) {
|
|
34
|
-
socialId = info.socialId
|
|
35
|
-
endpoint = info.endpoint
|
|
36
|
-
wsToken = info.token
|
|
37
|
-
workspaceUuid = String(info.workspace ?? '')
|
|
38
|
-
} else if ('token' in info && info.token) {
|
|
39
|
-
socialId = info.socialId
|
|
40
|
-
const wsInfo = await authedClient.selectWorkspace(workspaceUrl, 'external')
|
|
41
|
-
if (!wsInfo.endpoint || !wsInfo.token) throw new Error(`Workspace '${workspaceUrl}' not accessible`)
|
|
42
|
-
endpoint = wsInfo.endpoint
|
|
43
|
-
wsToken = wsInfo.token
|
|
44
|
-
workspaceUuid = String(wsInfo.workspace ?? '')
|
|
45
|
-
} else {
|
|
46
|
-
throw new Error('Unexpected token response')
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const rawConnection = await createClient(endpoint, wsToken)
|
|
50
|
-
const txClient = new TxOperations(rawConnection, socialId)
|
|
51
|
-
return { txClient, rawConnection, wsToken, workspaceUuid }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ── Upload blob ───────────────────────────────────────────────────────────────
|
|
55
|
-
|
|
56
|
-
async function uploadContent (wsToken, workspaceUuid, docId, content) {
|
|
57
|
-
const blobId = `${docId}-content-${Date.now()}`
|
|
58
|
-
const form = new FormData()
|
|
59
|
-
form.append('file', new Blob([content], { type: 'application/json' }), blobId)
|
|
60
|
-
|
|
61
|
-
const res = await fetch(`${DATALAKE_URL}/upload/form-data/${workspaceUuid}`, {
|
|
62
|
-
method: 'POST',
|
|
63
|
-
headers: { Authorization: `Bearer ${wsToken}` },
|
|
64
|
-
body: form
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
if (!res.ok) throw new Error(`Upload failed (${res.status}): ${await res.text()}`)
|
|
68
|
-
const json = await res.json()
|
|
69
|
-
return json[0]?.id ?? blobId
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ── Markdown → ProseMirror ────────────────────────────────────────────────────
|
|
73
|
-
|
|
74
|
-
function parseInline (text) {
|
|
75
|
-
const nodes = []
|
|
76
|
-
const re = /\*\*(.+?)\*\*|`(.+?)`|([^`*]+)/g
|
|
77
|
-
let m
|
|
78
|
-
while ((m = re.exec(text)) !== null) {
|
|
79
|
-
if (m[1] != null) nodes.push({ type: 'text', text: m[1], marks: [{ type: 'bold' }] })
|
|
80
|
-
else if (m[2] != null) nodes.push({ type: 'text', text: m[2], marks: [{ type: 'code' }] })
|
|
81
|
-
else if (m[3]?.length > 0) nodes.push({ type: 'text', text: m[3] })
|
|
82
|
-
}
|
|
83
|
-
return nodes.length > 0 ? nodes : [{ type: 'text', text }]
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function buildTable (rows) {
|
|
87
|
-
const [headerRow, ...bodyRows] = rows
|
|
88
|
-
const tableRows = []
|
|
89
|
-
if (headerRow) {
|
|
90
|
-
tableRows.push({
|
|
91
|
-
type: 'tableRow',
|
|
92
|
-
content: headerRow.map(cell => ({
|
|
93
|
-
type: 'tableHeader',
|
|
94
|
-
content: [{ type: 'paragraph', content: parseInline(cell) }]
|
|
95
|
-
}))
|
|
96
|
-
})
|
|
97
|
-
}
|
|
98
|
-
for (const row of bodyRows) {
|
|
99
|
-
tableRows.push({
|
|
100
|
-
type: 'tableRow',
|
|
101
|
-
content: row.map(cell => ({
|
|
102
|
-
type: 'tableCell',
|
|
103
|
-
content: [{ type: 'paragraph', content: parseInline(cell) }]
|
|
104
|
-
}))
|
|
105
|
-
})
|
|
106
|
-
}
|
|
107
|
-
return { type: 'table', content: tableRows }
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function markdownToProseMirror (md) {
|
|
111
|
-
const lines = md.split('\n')
|
|
112
|
-
const nodes = []
|
|
113
|
-
let i = 0
|
|
114
|
-
|
|
115
|
-
while (i < lines.length) {
|
|
116
|
-
const line = lines[i]
|
|
117
|
-
|
|
118
|
-
// Fenced code block
|
|
119
|
-
if (line.startsWith('```')) {
|
|
120
|
-
const lang = line.slice(3).trim()
|
|
121
|
-
const codeLines = []
|
|
122
|
-
i++
|
|
123
|
-
while (i < lines.length && !lines[i].startsWith('```')) {
|
|
124
|
-
codeLines.push(lines[i])
|
|
125
|
-
i++
|
|
126
|
-
}
|
|
127
|
-
i++
|
|
128
|
-
// Use Huly's native 'mermaid' node type so diagrams render properly
|
|
129
|
-
const nodeType = lang === 'mermaid' ? 'mermaid' : 'codeBlock'
|
|
130
|
-
nodes.push({
|
|
131
|
-
type: nodeType,
|
|
132
|
-
attrs: { language: lang || null },
|
|
133
|
-
content: [{ type: 'text', text: codeLines.join('\n') }]
|
|
134
|
-
})
|
|
135
|
-
continue
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Heading
|
|
139
|
-
const hm = line.match(/^(#{1,6})\s+(.+)$/)
|
|
140
|
-
if (hm) {
|
|
141
|
-
nodes.push({ type: 'heading', attrs: { level: hm[1].length }, content: parseInline(hm[2]) })
|
|
142
|
-
i++; continue
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Table
|
|
146
|
-
if (line.includes('|') && line.trim().startsWith('|')) {
|
|
147
|
-
const tableRows = []
|
|
148
|
-
while (i < lines.length && lines[i].includes('|') && lines[i].trim().startsWith('|')) {
|
|
149
|
-
const row = lines[i].trim().replace(/^\||\|$/g, '').split('|').map(c => c.trim())
|
|
150
|
-
if (!row.every(c => /^[-:]+$/.test(c))) tableRows.push(row)
|
|
151
|
-
i++
|
|
152
|
-
}
|
|
153
|
-
if (tableRows.length > 0) nodes.push(buildTable(tableRows))
|
|
154
|
-
continue
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Bullet list
|
|
158
|
-
if (/^(\s*[-*+])\s/.test(line)) {
|
|
159
|
-
const items = []
|
|
160
|
-
while (i < lines.length && /^(\s*[-*+])\s/.test(lines[i])) {
|
|
161
|
-
const text = lines[i].replace(/^\s*[-*+]\s/, '')
|
|
162
|
-
items.push({ type: 'listItem', content: [{ type: 'paragraph', content: parseInline(text) }] })
|
|
163
|
-
i++
|
|
164
|
-
}
|
|
165
|
-
nodes.push({ type: 'bulletList', content: items })
|
|
166
|
-
continue
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (line.trim() === '') { i++; continue }
|
|
170
|
-
|
|
171
|
-
nodes.push({ type: 'paragraph', content: parseInline(line) })
|
|
172
|
-
i++
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (nodes.length === 0) nodes.push({ type: 'paragraph', content: [] })
|
|
176
|
-
return { type: 'doc', content: nodes }
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// ── Document content ──────────────────────────────────────────────────────────
|
|
180
|
-
|
|
181
|
-
const EP1_MD = `# EP1 — Service Ordering Flow
|
|
182
|
-
|
|
183
|
-
## Architecture Flow
|
|
184
|
-
|
|
185
|
-
\`\`\`mermaid
|
|
186
|
-
flowchart TD
|
|
187
|
-
A([Patient Opens App]) --> B{What to order?}
|
|
188
|
-
|
|
189
|
-
B --> C[Health Packages\\ne.g. Full Body, Cardiac]
|
|
190
|
-
B --> D[Individual Lab Tests\\ne.g. CBC, HbA1c]
|
|
191
|
-
|
|
192
|
-
C --> E[Browse Catalogue]
|
|
193
|
-
D --> E
|
|
194
|
-
|
|
195
|
-
E --> F[Select Service]
|
|
196
|
-
F --> G[Select Patient\\nSelf or Family Member]
|
|
197
|
-
G --> H{Home Collection\\nAvailable?}
|
|
198
|
-
|
|
199
|
-
H -- Yes --> I[Choose: Home / Walk-in]
|
|
200
|
-
H -- No --> J[Walk-in Only]
|
|
201
|
-
|
|
202
|
-
I --> K[Pick Date & Slot]
|
|
203
|
-
J --> K
|
|
204
|
-
|
|
205
|
-
K --> L[Review Order]
|
|
206
|
-
L --> M[Payment via Razorpay]
|
|
207
|
-
|
|
208
|
-
M -- Success --> N[Order Confirmed ✅]
|
|
209
|
-
M -- Failed --> O[Retry Payment ❌]
|
|
210
|
-
|
|
211
|
-
N --> P[Sync to HIS]
|
|
212
|
-
N --> Q[Push Notification Sent]
|
|
213
|
-
N --> R[Order Tracking Active]
|
|
214
|
-
|
|
215
|
-
R --> S[CONFIRMED]
|
|
216
|
-
S --> T[SAMPLE COLLECTED]
|
|
217
|
-
T --> U[PROCESSING]
|
|
218
|
-
U --> V[REPORT READY 🎉]
|
|
219
|
-
V --> W[View Report in App]
|
|
220
|
-
\`\`\`
|
|
221
|
-
|
|
222
|
-
## Business Rules
|
|
223
|
-
|
|
224
|
-
| Rule | Detail |
|
|
225
|
-
|------|--------|
|
|
226
|
-
| Service visibility | Only services flagged \`canBeOrderedFromApp: true\` in HIS |
|
|
227
|
-
| Home collection | Checked per service + patient pin code |
|
|
228
|
-
| Payment | Mandatory before order confirmation |
|
|
229
|
-
| HIS sync | All orders synced back to HIS for lab/collection team |
|
|
230
|
-
| Family orders | All linked family members eligible |
|
|
231
|
-
`
|
|
232
|
-
|
|
233
|
-
const EP2_MD = `# EP2 — Medicine Ordering & Refills
|
|
234
|
-
|
|
235
|
-
## Flow
|
|
236
|
-
|
|
237
|
-
\`\`\`mermaid
|
|
238
|
-
flowchart TD
|
|
239
|
-
A([Entry Point]) --> B{Source}
|
|
240
|
-
|
|
241
|
-
B -- From Visit --> C[Prescription Medicines]
|
|
242
|
-
B -- Past Order --> D[Order History]
|
|
243
|
-
B -- Refill Reminder --> E[Refill Screen]
|
|
244
|
-
|
|
245
|
-
C --> F[Medicine List]
|
|
246
|
-
D --> F
|
|
247
|
-
E --> F
|
|
248
|
-
|
|
249
|
-
F --> G{Medicine Type?}
|
|
250
|
-
|
|
251
|
-
G -- OTC --> H[✅ Allow Order\\nNo Rx needed]
|
|
252
|
-
G -- Rx-Required --> I{Valid Prescription\\nExists?}
|
|
253
|
-
|
|
254
|
-
I -- Yes, Active --> J{Is it a Refill?}
|
|
255
|
-
I -- No / Expired --> K[❌ BLOCKED\\nShow: Prescription required\\nOffer: Book Follow-up]
|
|
256
|
-
|
|
257
|
-
J -- Within Validity --> L[✅ Allow Refill]
|
|
258
|
-
J -- Expired --> M[❌ BLOCKED\\nShow: Prescription expired\\nOffer: Book Follow-up]
|
|
259
|
-
|
|
260
|
-
H --> N[Select Patient]
|
|
261
|
-
L --> N
|
|
262
|
-
|
|
263
|
-
N --> O[Choose Delivery Address\\nSaved or New]
|
|
264
|
-
O --> P[Review Cart]
|
|
265
|
-
P --> Q[Payment]
|
|
266
|
-
|
|
267
|
-
Q -- Success --> R[Order Confirmed ✅]
|
|
268
|
-
Q -- Failed --> S[Retry ❌]
|
|
269
|
-
|
|
270
|
-
R --> T[PENDING]
|
|
271
|
-
T --> U[CONFIRMED]
|
|
272
|
-
U --> V[DISPATCHED 🚚]
|
|
273
|
-
V --> W[DELIVERED 🎉]
|
|
274
|
-
\`\`\`
|
|
275
|
-
|
|
276
|
-
## Prescription Validation Matrix
|
|
277
|
-
|
|
278
|
-
| Medicine Type | Valid Rx | Expired Rx | No Rx |
|
|
279
|
-
|--------------|----------|------------|-------|
|
|
280
|
-
| Rx-Required | ✅ Allow | ❌ Block + suggest follow-up | ❌ Block |
|
|
281
|
-
| OTC | ✅ Allow | ✅ Allow | ✅ Allow |
|
|
282
|
-
| Refill | ✅ Allow (within validity) | ❌ Block | ❌ Block |
|
|
283
|
-
|
|
284
|
-
## Business Rules
|
|
285
|
-
|
|
286
|
-
- Prescription validity period set at time of issue (30 / 60 / 90 days)
|
|
287
|
-
- Partial orders allowed: OTC items proceed even if Rx items are blocked
|
|
288
|
-
- Expired prescription → prompt to book a follow-up appointment
|
|
289
|
-
- All orders linked to patient + family member context
|
|
290
|
-
`
|
|
291
|
-
|
|
292
|
-
const EP3_MD = `# EP3 — Deep Linking Architecture
|
|
293
|
-
|
|
294
|
-
## Link Resolution Flow
|
|
295
|
-
|
|
296
|
-
\`\`\`mermaid
|
|
297
|
-
flowchart TD
|
|
298
|
-
A([Link Received\\nSMS / WhatsApp / Email / Push]) --> B[Smart Link URL\\nhttps://novacare.medicsprime.in/link/...]
|
|
299
|
-
|
|
300
|
-
B --> C{App Installed?}
|
|
301
|
-
|
|
302
|
-
C -- Yes --> D[Open Native App\\niOS Universal Link\\nAndroid App Link]
|
|
303
|
-
C -- No --> E[Open in Browser\\nWeb Fallback]
|
|
304
|
-
|
|
305
|
-
D --> F[Native Deep Link Handler]
|
|
306
|
-
F --> G{Authenticated?}
|
|
307
|
-
|
|
308
|
-
G -- Yes --> L[Route to Screen]
|
|
309
|
-
G -- No --> H[OTP Login\\nPre-fill phone if known]
|
|
310
|
-
H --> L
|
|
311
|
-
|
|
312
|
-
E --> I{Auto-Login Token\\nin URL?}
|
|
313
|
-
|
|
314
|
-
I -- Valid Token --> J[Auto-Login\\nSingle-use, 24h expiry]
|
|
315
|
-
I -- No / Expired --> K[OTP Login Page\\nPre-fill phone if known]
|
|
316
|
-
|
|
317
|
-
J --> L
|
|
318
|
-
K --> L
|
|
319
|
-
|
|
320
|
-
L --> M{Link Type}
|
|
321
|
-
|
|
322
|
-
M -- /book --> N[Appointment Booking\\npre-filled doctor/speciality]
|
|
323
|
-
M -- /apt --> O[Appointment Detail]
|
|
324
|
-
M -- /rx --> P[Prescription View]
|
|
325
|
-
M -- /report --> Q[Lab Report View]
|
|
326
|
-
M -- /package --> R[Health Package Order]
|
|
327
|
-
M -- /refill --> S[Medicine Refill Flow]
|
|
328
|
-
M -- /bill --> T[Bill & Payment]
|
|
329
|
-
\`\`\`
|
|
330
|
-
|
|
331
|
-
## Link Format
|
|
332
|
-
|
|
333
|
-
\`\`\`
|
|
334
|
-
https://{hospital}.medicsprime.in/link/{type}?id={resourceId}&t={autoLoginToken}&ph={phoneHint}
|
|
335
|
-
\`\`\`
|
|
336
|
-
|
|
337
|
-
| Parameter | Description |
|
|
338
|
-
|-----------|-------------|
|
|
339
|
-
| \`hospital\` | novacare / sarji / cura / khushi |
|
|
340
|
-
| \`type\` | apt / rx / report / package / refill / bill / book |
|
|
341
|
-
| \`id\` | Resource ID (optional for /book) |
|
|
342
|
-
| \`t\` | Auto-login token (24h, single-use, optional) |
|
|
343
|
-
| \`ph\` | Phone number hint for OTP pre-fill (optional) |
|
|
344
|
-
|
|
345
|
-
## Auto-Login Token Security
|
|
346
|
-
|
|
347
|
-
\`\`\`mermaid
|
|
348
|
-
flowchart LR
|
|
349
|
-
A[Token Generated] --> B[Signed with HMAC-SHA256]
|
|
350
|
-
B --> C[Contains: userId + expiry + resourceId]
|
|
351
|
-
C --> D[24-hour expiry]
|
|
352
|
-
D --> E[Single-use — invalidated on first use]
|
|
353
|
-
E --> F[Scoped to linked resource only]
|
|
354
|
-
\`\`\`
|
|
355
|
-
|
|
356
|
-
## Per-Hospital URL Schemes
|
|
357
|
-
|
|
358
|
-
| Hospital | Web Domain | Native Scheme |
|
|
359
|
-
|----------|-----------|---------------|
|
|
360
|
-
| NovaCare | novacare.medicsprime.in | novacare:// |
|
|
361
|
-
| Sarji | sarji.medicsprime.in | sarji:// |
|
|
362
|
-
| Cura | cura.medicsprime.in | cura:// |
|
|
363
|
-
| Khushi | khushi.medicsprime.in | khushi:// |
|
|
364
|
-
`
|
|
365
|
-
|
|
366
|
-
const EP4_MD = `# EP4 — Push Notifications
|
|
367
|
-
|
|
368
|
-
## Notification Delivery Architecture
|
|
369
|
-
|
|
370
|
-
\`\`\`mermaid
|
|
371
|
-
flowchart TD
|
|
372
|
-
A([Trigger]) --> B{Trigger Type}
|
|
373
|
-
|
|
374
|
-
B -- Hospital Event --> C[Appointment Confirmed\\nCancelled / Lab Ready\\nPrescription Ready\\nBill Generated]
|
|
375
|
-
B -- Scheduled Job --> D[Appointment Reminder\\nMedicine Dose\\nRefill Due\\nRx Expiry\\nCheckup Due]
|
|
376
|
-
B -- Patient Action --> E[Payment Success\\nOrder Placed\\nMedicine Ordered]
|
|
377
|
-
|
|
378
|
-
C --> F[Notification Engine\\nBackend Service]
|
|
379
|
-
D --> F
|
|
380
|
-
E --> F
|
|
381
|
-
|
|
382
|
-
F --> G{Check Opt-in\\nPreferences}
|
|
383
|
-
G -- Opted Out --> Z[Skip ✗]
|
|
384
|
-
G -- Opted In --> H{Quiet Hours?\\n10pm – 7am}
|
|
385
|
-
|
|
386
|
-
H -- Critical Alert --> I[Send Immediately\\nAppt Cancelled only]
|
|
387
|
-
H -- Non-Critical + Quiet --> J[Queue for 7am]
|
|
388
|
-
H -- Normal Hours --> K{Daily Limit\\nReached? max 3}
|
|
389
|
-
|
|
390
|
-
K -- Under Limit --> L[Send Now]
|
|
391
|
-
K -- At Limit --> M[Queue for Next Day]
|
|
392
|
-
|
|
393
|
-
I --> N[FCM / Firebase]
|
|
394
|
-
J --> N
|
|
395
|
-
L --> N
|
|
396
|
-
|
|
397
|
-
N --> O{Platform}
|
|
398
|
-
O -- iOS/Android --> P[Capacitor Push\\nNative Alert]
|
|
399
|
-
O -- Web --> Q[Firebase VAPID\\nBrowser Notification]
|
|
400
|
-
O -- Critical Only --> R[SMS Fallback\\nvia Communication Service]
|
|
401
|
-
|
|
402
|
-
P --> S[Tap → Deep Link\\nto Relevant Screen]
|
|
403
|
-
Q --> S
|
|
404
|
-
\`\`\`
|
|
405
|
-
|
|
406
|
-
## All Notification Scenarios
|
|
407
|
-
|
|
408
|
-
\`\`\`mermaid
|
|
409
|
-
mindmap
|
|
410
|
-
root((Push\\nNotifications))
|
|
411
|
-
Appointments
|
|
412
|
-
Confirmed ✅
|
|
413
|
-
Reminder 24h ⏰
|
|
414
|
-
Reminder 2h ⏰
|
|
415
|
-
Cancelled ❗
|
|
416
|
-
Rescheduled ❗
|
|
417
|
-
Doctor Late 🕐
|
|
418
|
-
Lab and Reports
|
|
419
|
-
Lab Result Ready 📋
|
|
420
|
-
Prescription Ready 💊
|
|
421
|
-
Home Collection Morning 🏠
|
|
422
|
-
Order Confirmed 📦
|
|
423
|
-
Medicine
|
|
424
|
-
Dose Reminder 💊
|
|
425
|
-
Missed Dose ⚠️
|
|
426
|
-
Refill Due 🔄
|
|
427
|
-
Rx Expiry 7 days 📅
|
|
428
|
-
Order Dispatched 🚚
|
|
429
|
-
Order Delivered 🎉
|
|
430
|
-
Billing
|
|
431
|
-
Bill Generated 💰
|
|
432
|
-
Payment Success ✅
|
|
433
|
-
Payment Due ⏳
|
|
434
|
-
Preventive Health
|
|
435
|
-
Annual Checkup 📅
|
|
436
|
-
Follow-up Due 👨⚕️
|
|
437
|
-
Vaccination Due 💉
|
|
438
|
-
\`\`\`
|
|
439
|
-
|
|
440
|
-
## Notification Preferences
|
|
441
|
-
|
|
442
|
-
\`\`\`mermaid
|
|
443
|
-
flowchart LR
|
|
444
|
-
A[Patient Settings] --> B[Notification Preferences]
|
|
445
|
-
B --> C[🔔 Appointments\\nDefault: ON]
|
|
446
|
-
B --> D[💊 Medicine\\nDefault: ON]
|
|
447
|
-
B --> E[📦 Orders\\nDefault: ON]
|
|
448
|
-
B --> F[💰 Billing\\nDefault: ON]
|
|
449
|
-
B --> G[🏥 Preventive Health\\nDefault: ON]
|
|
450
|
-
B --> H[📣 Promotional\\nDefault: OFF]
|
|
451
|
-
\`\`\`
|
|
452
|
-
|
|
453
|
-
## Scenario Reference Table
|
|
454
|
-
|
|
455
|
-
| Scenario | Trigger | Channel | Priority | Deep Link |
|
|
456
|
-
|----------|---------|---------|----------|-----------|
|
|
457
|
-
| Appt Confirmed | Event | Push | High | /apt?id=X |
|
|
458
|
-
| Appt Reminder 24h | Scheduled | Push + SMS | High | /apt?id=X |
|
|
459
|
-
| Appt Reminder 2h | Scheduled | Push | High | /apt?id=X |
|
|
460
|
-
| Appt Cancelled | Event | Push + SMS | **Critical** | /apt?id=X |
|
|
461
|
-
| Doctor Running Late | Event | Push | Normal | /apt?id=X |
|
|
462
|
-
| Lab Result Ready | Event | Push | High | /report?id=X |
|
|
463
|
-
| Prescription Ready | Event | Push | High | /rx?id=X |
|
|
464
|
-
| Medicine Dose Due | Scheduled | Push | Normal | Reminder |
|
|
465
|
-
| Missed Dose | Scheduled | Push | Normal | Reminder |
|
|
466
|
-
| Refill Due | Scheduled | Push | High | /refill?med=X |
|
|
467
|
-
| Rx Expiry (7 days) | Scheduled | Push | High | /rx?id=X |
|
|
468
|
-
| Bill Generated | Event | Push | High | /bill?id=X |
|
|
469
|
-
| Payment Success | Event | Push | Normal | /bill?id=X |
|
|
470
|
-
| Payment Due | Scheduled | Push | Normal | /bill?id=X |
|
|
471
|
-
| Annual Checkup | Scheduled | Push | Low | Book |
|
|
472
|
-
| Follow-up Due | Scheduled | Push | Normal | Book |
|
|
473
|
-
| Vaccination Due | Scheduled | Push | Normal | Reminder |
|
|
474
|
-
`
|
|
475
|
-
|
|
476
|
-
// ── Document IDs (created in previous session) ────────────────────────────────
|
|
477
|
-
const DOCS = [
|
|
478
|
-
{ id: '69b9a635cf60316d6ce21a50', title: 'EP1 — Service Ordering', md: EP1_MD },
|
|
479
|
-
{ id: '69b9a63bcf60316d6ce21a52', title: 'EP2 — Medicine Ordering', md: EP2_MD },
|
|
480
|
-
{ id: '69b9a640cf60316d6ce21a54', title: 'EP3 — Deep Linking', md: EP3_MD },
|
|
481
|
-
{ id: '69b9a645cf60316d6ce21a56', title: 'EP4 — Push Notifications', md: EP4_MD }
|
|
482
|
-
]
|
|
483
|
-
|
|
484
|
-
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
485
|
-
|
|
486
|
-
const { txClient, rawConnection, wsToken, workspaceUuid } = await connect()
|
|
487
|
-
console.log(`Connected. Workspace UUID: ${workspaceUuid}`)
|
|
488
|
-
|
|
489
|
-
for (const { id, title, md } of DOCS) {
|
|
490
|
-
process.stdout.write(`Updating "${title}" ... `)
|
|
491
|
-
try {
|
|
492
|
-
const doc = await txClient.findOne(document.class.Document, { _id: id })
|
|
493
|
-
if (!doc) { console.log('NOT FOUND — skipping'); continue }
|
|
494
|
-
|
|
495
|
-
const prosemirror = markdownToProseMirror(md)
|
|
496
|
-
const blobId = await uploadContent(wsToken, workspaceUuid, id, JSON.stringify(prosemirror))
|
|
497
|
-
await txClient.updateDoc(document.class.Document, doc.space, doc._id, { content: blobId })
|
|
498
|
-
console.log(`✅ blob: ${blobId}`)
|
|
499
|
-
} catch (err) {
|
|
500
|
-
console.log(`❌ ${err.message}`)
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
await rawConnection.close()
|
|
505
|
-
console.log('\nDone.')
|