huly-mcp-selfhost 1.0.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.
- package/.env.example +16 -0
- package/README.md +522 -0
- package/dist/connection.d.ts +9 -0
- package/dist/connection.js +122 -0
- package/dist/connection.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas.d.ts +459 -0
- package/dist/schemas.js +196 -0
- package/dist/schemas.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +87 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/attachments.d.ts +28 -0
- package/dist/tools/attachments.js +61 -0
- package/dist/tools/attachments.js.map +1 -0
- package/dist/tools/channels.d.ts +44 -0
- package/dist/tools/channels.js +130 -0
- package/dist/tools/channels.js.map +1 -0
- package/dist/tools/comments.d.ts +27 -0
- package/dist/tools/comments.js +60 -0
- package/dist/tools/comments.js.map +1 -0
- package/dist/tools/components.d.ts +19 -0
- package/dist/tools/components.js +57 -0
- package/dist/tools/components.js.map +1 -0
- package/dist/tools/documents.d.ts +68 -0
- package/dist/tools/documents.js +207 -0
- package/dist/tools/documents.js.map +1 -0
- package/dist/tools/issues.d.ts +56 -0
- package/dist/tools/issues.js +262 -0
- package/dist/tools/issues.js.map +1 -0
- package/dist/tools/labels.d.ts +35 -0
- package/dist/tools/labels.js +150 -0
- package/dist/tools/labels.js.map +1 -0
- package/dist/tools/log-time.d.ts +10 -0
- package/dist/tools/log-time.js +31 -0
- package/dist/tools/log-time.js.map +1 -0
- package/dist/tools/members.d.ts +6 -0
- package/dist/tools/members.js +17 -0
- package/dist/tools/members.js.map +1 -0
- package/dist/tools/milestones.d.ts +19 -0
- package/dist/tools/milestones.js +80 -0
- package/dist/tools/milestones.js.map +1 -0
- package/dist/tools/organizations.d.ts +32 -0
- package/dist/tools/organizations.js +86 -0
- package/dist/tools/organizations.js.map +1 -0
- package/dist/tools/projects.d.ts +24 -0
- package/dist/tools/projects.js +64 -0
- package/dist/tools/projects.js.map +1 -0
- package/dist/tools/relations.d.ts +27 -0
- package/dist/tools/relations.js +103 -0
- package/dist/tools/relations.js.map +1 -0
- package/dist/tools/search.d.ts +9 -0
- package/dist/tools/search.js +32 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/statuses.d.ts +16 -0
- package/dist/tools/statuses.js +93 -0
- package/dist/tools/statuses.js.map +1 -0
- package/dist/utils/errors.d.ts +11 -0
- package/dist/utils/errors.js +24 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/format.d.ts +4 -0
- package/dist/utils/format.js +28 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/markdown.d.ts +14 -0
- package/dist/utils/markdown.js +144 -0
- package/dist/utils/markdown.js.map +1 -0
- package/dist/utils/storage.d.ts +7 -0
- package/dist/utils/storage.js +82 -0
- package/dist/utils/storage.js.map +1 -0
- package/package.json +68 -0
- package/scripts/cleanup-issues.js +132 -0
- package/scripts/demo.js +193 -0
- package/scripts/import-csv.js +242 -0
- package/scripts/sample-tasks.csv +11 -0
- package/scripts/setup.js +103 -0
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "huly-mcp-selfhost",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Huly (self-hosted + cloud) ā connect Claude Desktop to your Huly workspace via the native WebSocket SDK. Fork of huly-mcp-sdk with self-hosted document/description support, chat, attachments, custom issue statuses, and organizations.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"huly",
|
|
7
|
+
"mcp",
|
|
8
|
+
"model-context-protocol",
|
|
9
|
+
"claude",
|
|
10
|
+
"project-management",
|
|
11
|
+
"issue-tracker",
|
|
12
|
+
"anthropic"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/JoKeks2023/huly-mcp#readme",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/JoKeks2023/huly-mcp.git"
|
|
18
|
+
},
|
|
19
|
+
"mcpName": "io.github.jokeks2023/huly-mcp-selfhost",
|
|
20
|
+
"license": "EPL-2.0",
|
|
21
|
+
"author": "SaiVaraprasad Medapati",
|
|
22
|
+
"contributors": [
|
|
23
|
+
"Joris Conrad (self-hosted fork: https://github.com/JoKeks2023/huly-mcp)"
|
|
24
|
+
],
|
|
25
|
+
"bin": {
|
|
26
|
+
"huly-mcp": "dist/index.js",
|
|
27
|
+
"huly-mcp-setup": "scripts/setup.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist/",
|
|
31
|
+
"scripts/",
|
|
32
|
+
".env.example",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "node --max-old-space-size=4096 node_modules/typescript/bin/tsc",
|
|
40
|
+
"dev": "tsx watch src/index.ts",
|
|
41
|
+
"start": "node dist/index.js",
|
|
42
|
+
"setup": "node scripts/setup.js",
|
|
43
|
+
"prepublishOnly": "npm run build"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@hcengineering/account-client": "^0.7.0",
|
|
47
|
+
"@hcengineering/attachment": "0.7.0",
|
|
48
|
+
"@hcengineering/chunter": "^0.7.0",
|
|
49
|
+
"@hcengineering/contact": "^0.7.0",
|
|
50
|
+
"@hcengineering/core": "^0.7.0",
|
|
51
|
+
"@hcengineering/document": "^0.7.0",
|
|
52
|
+
"@hcengineering/platform": "^0.7.0",
|
|
53
|
+
"@hcengineering/rank": "^0.7.0",
|
|
54
|
+
"@hcengineering/server-client": "^0.7.0",
|
|
55
|
+
"@hcengineering/tags": "^0.7.0",
|
|
56
|
+
"@hcengineering/task": "^0.7.0",
|
|
57
|
+
"@hcengineering/tracker": "^0.7.0",
|
|
58
|
+
"@modelcontextprotocol/sdk": "^1.10.0",
|
|
59
|
+
"ws": "^8.18.0",
|
|
60
|
+
"zod": "^3.23.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/node": "^22.0.0",
|
|
64
|
+
"@types/ws": "^8.5.0",
|
|
65
|
+
"tsx": "^4.19.0",
|
|
66
|
+
"typescript": "^5.7.0"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Delete a range of issues from a Huly project.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/cleanup-issues.js <project> <start> <end> [--confirm]
|
|
7
|
+
*
|
|
8
|
+
* Example (dry run ā shows what would be deleted):
|
|
9
|
+
* node scripts/cleanup-issues.js VISIO 59 68
|
|
10
|
+
*
|
|
11
|
+
* Example (actually delete):
|
|
12
|
+
* node scripts/cleanup-issues.js VISIO 59 68 --confirm
|
|
13
|
+
*
|
|
14
|
+
* Auth is read from .env (HULY_TOKEN + HULY_WORKSPACE, or HULY_EMAIL + HULY_PASSWORD).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict'
|
|
18
|
+
|
|
19
|
+
const fs = require('fs')
|
|
20
|
+
const path = require('path')
|
|
21
|
+
|
|
22
|
+
// Load .env
|
|
23
|
+
const envPath = path.join(__dirname, '..', '.env')
|
|
24
|
+
if (fs.existsSync(envPath)) {
|
|
25
|
+
for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) {
|
|
26
|
+
const m = line.match(/^([^#=\s]+)\s*=\s*(.*)$/)
|
|
27
|
+
if (m) process.env[m[1]] = m[2].trim()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Load compiled modules
|
|
32
|
+
const distDir = path.join(__dirname, '..', 'dist')
|
|
33
|
+
if (!fs.existsSync(path.join(distDir, 'connection.js'))) {
|
|
34
|
+
console.error('ā dist/ not found. Run `npm run build` first.')
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
37
|
+
const { getConnection, closeConnection } = require(path.join(distDir, 'connection'))
|
|
38
|
+
const tracker = require('@hcengineering/tracker')
|
|
39
|
+
|
|
40
|
+
// Parse args
|
|
41
|
+
const args = process.argv.slice(2)
|
|
42
|
+
const confirm = args.includes('--confirm')
|
|
43
|
+
const positional = args.filter(a => !a.startsWith('--'))
|
|
44
|
+
|
|
45
|
+
const [projectIdentifier, startStr, endStr] = positional
|
|
46
|
+
if (!projectIdentifier || !startStr || !endStr) {
|
|
47
|
+
console.error('Usage: node scripts/cleanup-issues.js <project> <start> <end> [--confirm]')
|
|
48
|
+
console.error('Example: node scripts/cleanup-issues.js VISIO 59 68 --confirm')
|
|
49
|
+
process.exit(1)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const start = parseInt(startStr, 10)
|
|
53
|
+
const end = parseInt(endStr, 10)
|
|
54
|
+
if (isNaN(start) || isNaN(end) || start > end) {
|
|
55
|
+
console.error('ā start and end must be valid numbers with start <= end')
|
|
56
|
+
process.exit(1)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function main () {
|
|
60
|
+
const identifiers = []
|
|
61
|
+
for (let n = start; n <= end; n++) {
|
|
62
|
+
identifiers.push(`${projectIdentifier}-${n}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`\nš Huly Issue Cleanup`)
|
|
66
|
+
console.log(` Project: ${projectIdentifier}`)
|
|
67
|
+
console.log(` Range: ${projectIdentifier}-${start} ā ${projectIdentifier}-${end} (${identifiers.length} issues)`)
|
|
68
|
+
console.log(` Mode: ${confirm ? 'ā ļø DELETING for real' : 'š DRY RUN (pass --confirm to delete)'}\n`)
|
|
69
|
+
|
|
70
|
+
console.log('š Connecting to Huly...')
|
|
71
|
+
const client = await getConnection()
|
|
72
|
+
console.log('ā
Connected.\n')
|
|
73
|
+
|
|
74
|
+
// Find all issues in the range
|
|
75
|
+
const issues = await client.findAll(
|
|
76
|
+
tracker.default.class.Issue,
|
|
77
|
+
{ identifier: { $in: identifiers } }
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if (issues.length === 0) {
|
|
81
|
+
console.log('ā
No matching issues found ā nothing to delete.')
|
|
82
|
+
await closeConnection()
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log(`Found ${issues.length} issue(s) to delete:\n`)
|
|
87
|
+
for (const issue of issues) {
|
|
88
|
+
console.log(` ⢠${issue.identifier} "${issue.title}"`)
|
|
89
|
+
}
|
|
90
|
+
console.log()
|
|
91
|
+
|
|
92
|
+
if (!confirm) {
|
|
93
|
+
console.log('ā¹ļø This was a DRY RUN. To actually delete, run:')
|
|
94
|
+
console.log(` node scripts/cleanup-issues.js ${projectIdentifier} ${start} ${end} --confirm\n`)
|
|
95
|
+
await closeConnection()
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Delete
|
|
100
|
+
let deleted = 0
|
|
101
|
+
let failed = 0
|
|
102
|
+
|
|
103
|
+
for (const issue of issues) {
|
|
104
|
+
process.stdout.write(` Deleting ${issue.identifier}... `)
|
|
105
|
+
try {
|
|
106
|
+
await client.removeCollection(
|
|
107
|
+
tracker.default.class.Issue,
|
|
108
|
+
issue.space,
|
|
109
|
+
issue._id,
|
|
110
|
+
issue.attachedTo,
|
|
111
|
+
issue.attachedToClass,
|
|
112
|
+
issue.collection
|
|
113
|
+
)
|
|
114
|
+
console.log('ā
')
|
|
115
|
+
deleted++
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.log(`ā ${err.message}`)
|
|
118
|
+
failed++
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await closeConnection()
|
|
123
|
+
|
|
124
|
+
console.log(`\n${'ā'.repeat(50)}`)
|
|
125
|
+
console.log(`ā
Deleted: ${deleted} ā Failed: ${failed}`)
|
|
126
|
+
console.log(`${'ā'.repeat(50)}\n`)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
main().catch(err => {
|
|
130
|
+
console.error('\nā Fatal error:', err.message)
|
|
131
|
+
process.exit(1)
|
|
132
|
+
})
|
package/scripts/demo.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Live demo of huly-mcp-sdk tools against a real Huly workspace.
|
|
4
|
+
* Runs through: list projects ā create issue ā add comment ā add label ā
|
|
5
|
+
* update issue ā add relation ā get issue ā delete issue
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict'
|
|
9
|
+
|
|
10
|
+
const fs = require('fs')
|
|
11
|
+
const path = require('path')
|
|
12
|
+
|
|
13
|
+
// Load .env
|
|
14
|
+
const envPath = path.join(__dirname, '..', '.env')
|
|
15
|
+
if (fs.existsSync(envPath)) {
|
|
16
|
+
for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) {
|
|
17
|
+
const m = line.match(/^([^#=\s]+)\s*=\s*(.*)$/)
|
|
18
|
+
if (m) process.env[m[1]] = m[2].trim()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const distDir = path.join(__dirname, '..', 'dist')
|
|
23
|
+
const { getConnection, closeConnection } = require(path.join(distDir, 'connection'))
|
|
24
|
+
const tracker = require('@hcengineering/tracker')
|
|
25
|
+
const task = require('@hcengineering/task')
|
|
26
|
+
const tags = require('@hcengineering/tags')
|
|
27
|
+
const { generateId, SortingOrder } = require('@hcengineering/core')
|
|
28
|
+
const { makeRank } = require('@hcengineering/rank')
|
|
29
|
+
|
|
30
|
+
const GREEN = '\x1b[32m'
|
|
31
|
+
const CYAN = '\x1b[36m'
|
|
32
|
+
const YELLOW = '\x1b[33m'
|
|
33
|
+
const BOLD = '\x1b[1m'
|
|
34
|
+
const RESET = '\x1b[0m'
|
|
35
|
+
|
|
36
|
+
function header (text) {
|
|
37
|
+
console.log(`\n${BOLD}${CYAN}āāā ${text} āāā${RESET}`)
|
|
38
|
+
}
|
|
39
|
+
function ok (text) { console.log(`${GREEN}ā
${text}${RESET}`) }
|
|
40
|
+
function info (text) { console.log(`${YELLOW} ${text}${RESET}`) }
|
|
41
|
+
function print (text) { console.log(` ${text}`) }
|
|
42
|
+
|
|
43
|
+
async function main () {
|
|
44
|
+
console.log(`\n${BOLD}š huly-mcp-sdk live demo${RESET}`)
|
|
45
|
+
console.log(` Workspace: ${process.env.HULY_WORKSPACE}\n`)
|
|
46
|
+
|
|
47
|
+
const client = await getConnection()
|
|
48
|
+
ok('Connected to Huly')
|
|
49
|
+
|
|
50
|
+
// āā 1. List projects āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
51
|
+
header('1 / 8 ā list_projects')
|
|
52
|
+
const projects = await client.findAll(tracker.default.class.Project, {})
|
|
53
|
+
ok(`Found ${projects.length} project(s)`)
|
|
54
|
+
for (const p of projects) {
|
|
55
|
+
print(`⢠${BOLD}${p.identifier}${RESET} ${p.name}`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Pick first project for the rest of the demo
|
|
59
|
+
const project = projects[0]
|
|
60
|
+
if (project == null) { console.error('No projects found ā aborting.'); return }
|
|
61
|
+
info(`Using project: ${project.identifier}`)
|
|
62
|
+
|
|
63
|
+
// āā 2. List issues (top 5) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
64
|
+
header('2 / 8 ā list_issues')
|
|
65
|
+
const issues = await client.findAll(
|
|
66
|
+
tracker.default.class.Issue,
|
|
67
|
+
{ space: project._id },
|
|
68
|
+
{ limit: 5, sort: { modifiedOn: SortingOrder.Descending } }
|
|
69
|
+
)
|
|
70
|
+
const statuses = await client.findAll(tracker.default.class.IssueStatus, {})
|
|
71
|
+
const statusMap = new Map(statuses.map(s => [s._id, s.name]))
|
|
72
|
+
ok(`Showing 5 most-recent issues in ${project.identifier}`)
|
|
73
|
+
for (const i of issues) {
|
|
74
|
+
print(`⢠${BOLD}${i.identifier}${RESET} [${statusMap.get(i.status) ?? '?'}] ${i.title}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// āā 3. Create issue āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
78
|
+
header('3 / 8 ā create_issue')
|
|
79
|
+
const incResult = await client.updateDoc(
|
|
80
|
+
tracker.default.class.Project, project.space, project._id,
|
|
81
|
+
{ $inc: { sequence: 1 } }, true
|
|
82
|
+
)
|
|
83
|
+
const issueNumber = incResult?.object?.sequence ?? project.sequence + 1
|
|
84
|
+
const identifier = `${project.identifier}-${issueNumber}`
|
|
85
|
+
|
|
86
|
+
const defaultStatus = project.defaultIssueStatus != null
|
|
87
|
+
? (statuses.find(s => s._id === project.defaultIssueStatus) ?? statuses[0])
|
|
88
|
+
: statuses[0]
|
|
89
|
+
const kind = await client.findOne(task.default.class.TaskType,
|
|
90
|
+
project.type != null ? { parent: project.type } : {})
|
|
91
|
+
const lastIssue = await client.findOne(
|
|
92
|
+
tracker.default.class.Issue, { space: project._id },
|
|
93
|
+
{ sort: { rank: SortingOrder.Descending } }
|
|
94
|
+
)
|
|
95
|
+
const rank = makeRank(lastIssue?.rank, undefined)
|
|
96
|
+
|
|
97
|
+
const issueId = generateId()
|
|
98
|
+
await client.addCollection(
|
|
99
|
+
tracker.default.class.Issue, project._id,
|
|
100
|
+
tracker.default.ids.NoParent, tracker.default.class.Issue, 'subIssues',
|
|
101
|
+
{
|
|
102
|
+
title: 'š§Ŗ huly-mcp-sdk demo issue',
|
|
103
|
+
description: null,
|
|
104
|
+
status: defaultStatus._id,
|
|
105
|
+
priority: 2, // High
|
|
106
|
+
number: issueNumber, identifier, rank,
|
|
107
|
+
kind: kind._id,
|
|
108
|
+
comments: 0, subIssues: 0,
|
|
109
|
+
dueDate: null, assignee: null, component: null, milestone: null,
|
|
110
|
+
parents: [], remainingTime: 0, estimation: 0,
|
|
111
|
+
reportedTime: 0, reports: 0, childInfo: [], relations: []
|
|
112
|
+
},
|
|
113
|
+
issueId
|
|
114
|
+
)
|
|
115
|
+
ok(`Created ${BOLD}${identifier}${RESET}: š§Ŗ huly-mcp-sdk demo issue`)
|
|
116
|
+
|
|
117
|
+
// āā 4. Add comment āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
118
|
+
header('4 / 8 ā add_comment')
|
|
119
|
+
const chunter = require('@hcengineering/chunter')
|
|
120
|
+
await client.addCollection(
|
|
121
|
+
chunter.default.class.ChatMessage, project._id,
|
|
122
|
+
issueId, tracker.default.class.Issue, 'comments',
|
|
123
|
+
{ message: 'This issue was created by the **huly-mcp-sdk** demo script š', attachments: 0 }
|
|
124
|
+
)
|
|
125
|
+
ok(`Added comment to ${identifier}`)
|
|
126
|
+
|
|
127
|
+
// āā 5. Add label āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
128
|
+
header('5 / 8 ā add_label')
|
|
129
|
+
const core = require('@hcengineering/core')
|
|
130
|
+
let label = await client.findOne(tags.default.class.TagElement, {
|
|
131
|
+
targetClass: tracker.default.class.Issue, title: 'demo'
|
|
132
|
+
})
|
|
133
|
+
if (label == null) {
|
|
134
|
+
const cat = await client.findOne(tags.default.class.TagCategory, {
|
|
135
|
+
targetClass: tracker.default.class.Issue
|
|
136
|
+
})
|
|
137
|
+
const labelId = generateId()
|
|
138
|
+
await client.createDoc(
|
|
139
|
+
tags.default.class.TagElement, core.default.space.Workspace,
|
|
140
|
+
{
|
|
141
|
+
title: 'demo', targetClass: tracker.default.class.Issue,
|
|
142
|
+
description: '', color: 0x6366f1,
|
|
143
|
+
category: cat?._id ?? tags.default.category.NoCategory, refCount: 0
|
|
144
|
+
}, labelId
|
|
145
|
+
)
|
|
146
|
+
label = await client.findOne(tags.default.class.TagElement, { _id: labelId })
|
|
147
|
+
info('Auto-created label "demo" (color: #6366f1)')
|
|
148
|
+
}
|
|
149
|
+
const issue = await client.findOne(tracker.default.class.Issue, { identifier })
|
|
150
|
+
await client.addCollection(
|
|
151
|
+
tags.default.class.TagReference, issue.space, issue._id,
|
|
152
|
+
tracker.default.class.Issue, 'labels',
|
|
153
|
+
{ tag: label._id, title: label.title, color: label.color }
|
|
154
|
+
)
|
|
155
|
+
ok(`Labeled ${identifier} with "demo"`)
|
|
156
|
+
|
|
157
|
+
// āā 6. Update issue āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
158
|
+
header('6 / 8 ā update_issue')
|
|
159
|
+
const inProgress = statuses.find(s => s.name.toLowerCase().includes('progress')) ?? statuses[1] ?? statuses[0]
|
|
160
|
+
await client.updateDoc(
|
|
161
|
+
tracker.default.class.Issue, issue.space, issue._id,
|
|
162
|
+
{ status: inProgress._id, priority: 1 /* Urgent */ }
|
|
163
|
+
)
|
|
164
|
+
ok(`Updated ${identifier} ā status: "${inProgress.name}", priority: Urgent`)
|
|
165
|
+
|
|
166
|
+
// āā 7. Get issue (full details) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
167
|
+
header('7 / 8 ā get_issue')
|
|
168
|
+
const fresh = await client.findOne(tracker.default.class.Issue, { identifier })
|
|
169
|
+
const freshStatus = statusMap.get(fresh.status) ?? 'Unknown'
|
|
170
|
+
const PRIORITY = ['No priority', 'Urgent', 'High', 'Medium', 'Low']
|
|
171
|
+
ok(`Full details for ${identifier}`)
|
|
172
|
+
print(` Title: ${fresh.title}`)
|
|
173
|
+
print(` Status: ${freshStatus}`)
|
|
174
|
+
print(` Priority: ${PRIORITY[fresh.priority] ?? fresh.priority}`)
|
|
175
|
+
print(` Comments: ${fresh.comments}`)
|
|
176
|
+
|
|
177
|
+
// āā 8. Delete issue (cleanup) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
178
|
+
header('8 / 8 ā delete_issue (cleanup)')
|
|
179
|
+
await client.removeCollection(
|
|
180
|
+
tracker.default.class.Issue, fresh.space, fresh._id,
|
|
181
|
+
fresh.attachedTo, fresh.attachedToClass, fresh.collection
|
|
182
|
+
)
|
|
183
|
+
ok(`Deleted ${identifier} ā workspace is clean`)
|
|
184
|
+
|
|
185
|
+
await closeConnection()
|
|
186
|
+
|
|
187
|
+
console.log(`\n${BOLD}${GREEN}š Demo complete! All 8 tools exercised successfully.${RESET}\n`)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
main().catch(err => {
|
|
191
|
+
console.error('\nā Demo failed:', err.message)
|
|
192
|
+
process.exit(1)
|
|
193
|
+
})
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Bulk-import issues from a CSV file into a Huly tracker project.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/import-csv.js <csv-file> <project-identifier>
|
|
7
|
+
*
|
|
8
|
+
* Example:
|
|
9
|
+
* node scripts/import-csv.js tasks.csv PROJ
|
|
10
|
+
*
|
|
11
|
+
* CSV format (first row = headers):
|
|
12
|
+
* title,priority,status,dueDate
|
|
13
|
+
* Fix login bug,High,In Progress,2025-04-01
|
|
14
|
+
* Add dark mode,Medium,,
|
|
15
|
+
*
|
|
16
|
+
* Required columns:
|
|
17
|
+
* title ā issue title (required)
|
|
18
|
+
*
|
|
19
|
+
* Optional columns:
|
|
20
|
+
* priority ā Urgent | High | Medium | Low | No priority (default: No priority)
|
|
21
|
+
* status ā name of an existing status in the project (default: project default)
|
|
22
|
+
* dueDate ā YYYY-MM-DD (default: none)
|
|
23
|
+
*
|
|
24
|
+
* Auth (set ONE option as env vars):
|
|
25
|
+
* Option A (SSO/Google/GitHub):
|
|
26
|
+
* HULY_TOKEN=eyJ... ā token from DevTools ā Application ā IndexedDB database name
|
|
27
|
+
*
|
|
28
|
+
* Option B (email + password):
|
|
29
|
+
* HULY_EMAIL=you@example.com
|
|
30
|
+
* HULY_PASSWORD=yourpassword
|
|
31
|
+
*
|
|
32
|
+
* Always required:
|
|
33
|
+
* HULY_WORKSPACE=your-workspace-slug
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
'use strict'
|
|
37
|
+
|
|
38
|
+
const fs = require('fs')
|
|
39
|
+
const path = require('path')
|
|
40
|
+
|
|
41
|
+
// āā Load compiled huly-mcp modules āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
42
|
+
const distDir = path.join(__dirname, '..', 'dist')
|
|
43
|
+
if (!fs.existsSync(path.join(distDir, 'connection.js'))) {
|
|
44
|
+
console.error('ā dist/ not found. Run `npm run build` first.')
|
|
45
|
+
process.exit(1)
|
|
46
|
+
}
|
|
47
|
+
const { getConnection, closeConnection } = require(path.join(distDir, 'connection'))
|
|
48
|
+
const tracker = require('@hcengineering/tracker')
|
|
49
|
+
const task = require('@hcengineering/task')
|
|
50
|
+
const { SortingOrder, generateId } = require('@hcengineering/core')
|
|
51
|
+
const { makeRank } = require('@hcengineering/rank')
|
|
52
|
+
|
|
53
|
+
// āā Parse CLI args āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
54
|
+
const [,, csvFile, projectIdentifier] = process.argv
|
|
55
|
+
if (!csvFile || !projectIdentifier) {
|
|
56
|
+
console.error('Usage: node scripts/import-csv.js <csv-file> <project-identifier>')
|
|
57
|
+
process.exit(1)
|
|
58
|
+
}
|
|
59
|
+
if (!fs.existsSync(csvFile)) {
|
|
60
|
+
console.error(`ā File not found: ${csvFile}`)
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// āā CSV parser (no dependencies) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
65
|
+
function parseCsv (text) {
|
|
66
|
+
const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n').filter(Boolean)
|
|
67
|
+
if (lines.length < 2) return []
|
|
68
|
+
|
|
69
|
+
const headers = lines[0].split(',').map(h => h.trim().toLowerCase())
|
|
70
|
+
return lines.slice(1).map(line => {
|
|
71
|
+
// Handle quoted fields
|
|
72
|
+
const fields = []
|
|
73
|
+
let current = ''
|
|
74
|
+
let inQuotes = false
|
|
75
|
+
for (const ch of line) {
|
|
76
|
+
if (ch === '"') { inQuotes = !inQuotes }
|
|
77
|
+
else if (ch === ',' && !inQuotes) { fields.push(current.trim()); current = '' }
|
|
78
|
+
else { current += ch }
|
|
79
|
+
}
|
|
80
|
+
fields.push(current.trim())
|
|
81
|
+
return Object.fromEntries(headers.map((h, i) => [h, fields[i] ?? '']))
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// āā Priority mapping āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
86
|
+
const PRIORITY_MAP = {
|
|
87
|
+
'urgent': 1, // IssuePriority.Urgent
|
|
88
|
+
'high': 2, // IssuePriority.High
|
|
89
|
+
'medium': 3, // IssuePriority.Medium
|
|
90
|
+
'low': 4, // IssuePriority.Low
|
|
91
|
+
'no priority': 0,
|
|
92
|
+
'': 0
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parsePriority (str) {
|
|
96
|
+
const key = (str || '').toLowerCase().trim()
|
|
97
|
+
return PRIORITY_MAP[key] ?? 0
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// āā Main import āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
101
|
+
async function main () {
|
|
102
|
+
console.log(`\nš Huly CSV Importer`)
|
|
103
|
+
console.log(` File: ${csvFile}`)
|
|
104
|
+
console.log(` Project: ${projectIdentifier}`)
|
|
105
|
+
console.log(` Workspace: ${process.env.HULY_WORKSPACE}\n`)
|
|
106
|
+
|
|
107
|
+
// Parse CSV
|
|
108
|
+
const rows = parseCsv(fs.readFileSync(csvFile, 'utf8'))
|
|
109
|
+
const validRows = rows.filter(r => r.title && r.title.trim())
|
|
110
|
+
console.log(`š Found ${rows.length} rows, ${validRows.length} with titles.\n`)
|
|
111
|
+
|
|
112
|
+
if (validRows.length === 0) {
|
|
113
|
+
console.error('ā No valid rows found. Make sure CSV has a "title" column.')
|
|
114
|
+
process.exit(1)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Connect to Huly
|
|
118
|
+
console.log('š Connecting to Huly...')
|
|
119
|
+
const client = await getConnection()
|
|
120
|
+
console.log('ā
Connected.\n')
|
|
121
|
+
|
|
122
|
+
// Find project
|
|
123
|
+
const project = await client.findOne(tracker.default.class.Project, { identifier: projectIdentifier })
|
|
124
|
+
if (project == null) {
|
|
125
|
+
console.error(`ā Project '${projectIdentifier}' not found.`)
|
|
126
|
+
await closeConnection()
|
|
127
|
+
process.exit(1)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Load statuses ā Huly stores them in project space OR global model space
|
|
131
|
+
let statuses = await client.findAll(tracker.default.class.IssueStatus, { space: project._id })
|
|
132
|
+
if (statuses.length === 0) {
|
|
133
|
+
// Fall back to global model-level statuses (used by default project types)
|
|
134
|
+
statuses = await client.findAll(tracker.default.class.IssueStatus, {})
|
|
135
|
+
}
|
|
136
|
+
const statusMap = new Map(statuses.map(s => [s.name.toLowerCase(), s]))
|
|
137
|
+
const defaultStatus = project.defaultIssueStatus != null
|
|
138
|
+
? (statuses.find(s => s._id === project.defaultIssueStatus) ?? statuses[0])
|
|
139
|
+
: statuses[0]
|
|
140
|
+
|
|
141
|
+
// Find TaskType
|
|
142
|
+
const kind = await client.findOne(task.default.class.TaskType,
|
|
143
|
+
project.type != null ? { parent: project.type } : {})
|
|
144
|
+
if (kind == null) {
|
|
145
|
+
console.error('ā Could not find a TaskType for this project.')
|
|
146
|
+
await closeConnection()
|
|
147
|
+
process.exit(1)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log(`š Project: ${project.name}`)
|
|
151
|
+
console.log(`š Available statuses: ${statuses.map(s => s.name).join(', ')}`)
|
|
152
|
+
console.log(`šÆ Default status: ${defaultStatus.name}\n`)
|
|
153
|
+
|
|
154
|
+
// Import rows
|
|
155
|
+
let created = 0
|
|
156
|
+
let failed = 0
|
|
157
|
+
|
|
158
|
+
for (let i = 0; i < validRows.length; i++) {
|
|
159
|
+
const row = validRows[i]
|
|
160
|
+
const num = i + 1
|
|
161
|
+
|
|
162
|
+
process.stdout.write(`[${num}/${validRows.length}] Creating: "${row.title.slice(0, 60)}"... `)
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// Increment sequence
|
|
166
|
+
const incResult = await client.updateDoc(
|
|
167
|
+
tracker.default.class.Project,
|
|
168
|
+
project.space,
|
|
169
|
+
project._id,
|
|
170
|
+
{ $inc: { sequence: 1 } },
|
|
171
|
+
true
|
|
172
|
+
)
|
|
173
|
+
const issueNumber = incResult?.object?.sequence ?? project.sequence + 1
|
|
174
|
+
const identifier = `${project.identifier}-${issueNumber}`
|
|
175
|
+
|
|
176
|
+
// Resolve status
|
|
177
|
+
const statusName = (row.status || '').toLowerCase().trim()
|
|
178
|
+
const status = statusName ? (statusMap.get(statusName) ?? defaultStatus) : defaultStatus
|
|
179
|
+
|
|
180
|
+
// Compute rank
|
|
181
|
+
const lastIssue = await client.findOne(
|
|
182
|
+
tracker.default.class.Issue,
|
|
183
|
+
{ space: project._id },
|
|
184
|
+
{ sort: { rank: SortingOrder.Descending } }
|
|
185
|
+
)
|
|
186
|
+
const rank = makeRank(lastIssue?.rank, undefined)
|
|
187
|
+
|
|
188
|
+
// Create issue
|
|
189
|
+
const issueId = generateId()
|
|
190
|
+
await client.addCollection(
|
|
191
|
+
tracker.default.class.Issue,
|
|
192
|
+
project._id,
|
|
193
|
+
tracker.default.ids.NoParent,
|
|
194
|
+
tracker.default.class.Issue,
|
|
195
|
+
'subIssues',
|
|
196
|
+
{
|
|
197
|
+
title: row.title.trim(),
|
|
198
|
+
description: null,
|
|
199
|
+
status: status._id,
|
|
200
|
+
priority: parsePriority(row.priority),
|
|
201
|
+
number: issueNumber,
|
|
202
|
+
identifier,
|
|
203
|
+
rank,
|
|
204
|
+
kind: kind._id,
|
|
205
|
+
comments: 0,
|
|
206
|
+
subIssues: 0,
|
|
207
|
+
dueDate: row.duedate || row.dueDate
|
|
208
|
+
? new Date(row.duedate || row.dueDate).getTime()
|
|
209
|
+
: null,
|
|
210
|
+
assignee: null,
|
|
211
|
+
component: null,
|
|
212
|
+
milestone: null,
|
|
213
|
+
parents: [],
|
|
214
|
+
remainingTime: 0,
|
|
215
|
+
estimation: 0,
|
|
216
|
+
reportedTime: 0,
|
|
217
|
+
reports: 0,
|
|
218
|
+
childInfo: [],
|
|
219
|
+
relations: []
|
|
220
|
+
},
|
|
221
|
+
issueId
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
console.log(`ā
${identifier}`)
|
|
225
|
+
created++
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.log(`ā ERROR: ${err.message}`)
|
|
228
|
+
failed++
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
await closeConnection()
|
|
233
|
+
|
|
234
|
+
console.log(`\n${'ā'.repeat(50)}`)
|
|
235
|
+
console.log(`ā
Created: ${created} ā Failed: ${failed}`)
|
|
236
|
+
console.log(`${'ā'.repeat(50)}\n`)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
main().catch(err => {
|
|
240
|
+
console.error('\nā Fatal error:', err.message)
|
|
241
|
+
process.exit(1)
|
|
242
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
title,priority,status,dueDate
|
|
2
|
+
Fix login timeout bug,High,In Progress,2025-04-01
|
|
3
|
+
Add dark mode support,Medium,,
|
|
4
|
+
Update onboarding flow,Low,,2025-05-15
|
|
5
|
+
Write API documentation,Medium,,
|
|
6
|
+
Fix mobile layout issues,High,,
|
|
7
|
+
Performance optimization for dashboard,Urgent,,2025-03-31
|
|
8
|
+
Add two-factor authentication,High,,
|
|
9
|
+
Update privacy policy page,Low,,
|
|
10
|
+
Fix broken image uploads,High,In Progress,
|
|
11
|
+
Improve search functionality,Medium,,
|