snow-flow 10.0.185 → 10.0.186-dev.682
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/bin/index.js.map +9 -9
- package/bin/worker.js.map +7 -7
- package/mcp/servicenow-unified.js +116 -116
- package/package.json +1 -1
- package/parsers-config.ts +2 -1
- package/src/bun/index.ts +10 -9
- package/src/cli/cmd/agent.ts +3 -3
- package/src/cli/cmd/auth.ts +46 -0
- package/src/cli/cmd/import.ts +2 -2
- package/src/cli/cmd/session.ts +9 -12
- package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +2 -1
- package/src/cli/cmd/tui/component/prompt/index.tsx +19 -6
- package/src/cli/cmd/tui/component/spinner.tsx +24 -0
- package/src/cli/cmd/tui/context/exit.tsx +1 -1
- package/src/cli/cmd/tui/routes/home.tsx +16 -2
- package/src/cli/cmd/tui/routes/session/index.tsx +122 -53
- package/src/cli/cmd/tui/routes/session/permission.tsx +9 -1
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +9 -1
- package/src/cli/cmd/tui/thread.ts +4 -1
- package/src/cli/cmd/tui/ui/dialog-export-options.tsx +1 -1
- package/src/cli/cmd/tui/util/clipboard.ts +3 -3
- package/src/cli/cmd/tui/worker.ts +6 -1
- package/src/config/config.ts +28 -0
- package/src/context/context-db.ts +437 -0
- package/src/format/formatter.ts +14 -5
- package/src/global/index.ts +3 -4
- package/src/mcp/index.ts +7 -2
- package/src/mcp/oauth-callback.ts +7 -15
- package/src/mcp/oauth-provider.ts +34 -3
- package/src/project/project.ts +8 -4
- package/src/provider/models.ts +1 -1
- package/src/provider/provider.ts +88 -9
- package/src/provider/transform.ts +7 -2
- package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_capacity_plan.ts +20 -7
- package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_retrospective.ts +6 -8
- package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_sprint_manage.ts +46 -28
- package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_team_manage.ts +53 -41
- package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_velocity_report.ts +8 -1
- package/src/servicenow/servicenow-mcp-unified/tools/automation/snow_schedule_script_job.ts +388 -243
- package/src/session/compaction.ts +126 -23
- package/src/session/message-v2.ts +33 -10
- package/src/session/processor.ts +29 -17
- package/src/session/prompt.ts +34 -6
- package/src/share/share-next.ts +2 -2
- package/src/shell/shell.ts +2 -1
- package/src/tool/edit.ts +15 -1
- package/src/tool/registry.ts +9 -1
- package/src/tool/truncation.ts +17 -0
- package/src/tool/websearch.ts +1 -1
- package/src/tool/websearch.txt +2 -2
- package/src/tool/write.ts +3 -4
- package/src/util/filesystem.ts +36 -7
- package/src/util/keybind.ts +1 -1
- package/src/util/log.ts +8 -5
- package/src/util/token.ts +28 -0
- package/test/cli/plugin-auth-picker.test.ts +120 -0
- package/test/fixture/fixture.ts +3 -0
- package/test/mcp/oauth-auto-connect.test.ts +197 -0
- package/test/project/project.test.ts +47 -0
- package/test/provider/provider.test.ts +2 -0
- package/test/provider/transform.test.ts +32 -0
- package/test/tool/edit.test.ts +679 -0
|
@@ -143,10 +143,41 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
143
143
|
|
|
144
144
|
async state(): Promise<string> {
|
|
145
145
|
const entry = await McpAuth.get(this.mcpName)
|
|
146
|
-
if (
|
|
147
|
-
|
|
146
|
+
if (entry?.oauthState) {
|
|
147
|
+
return entry.oauthState
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Generate a new state if none exists — the SDK calls state() as a
|
|
151
|
+
// generator, not just a reader, so we need to produce a value even when
|
|
152
|
+
// startAuth() hasn't pre-saved one (e.g. during automatic auth on first
|
|
153
|
+
// connect).
|
|
154
|
+
const newState = Array.from(crypto.getRandomValues(new Uint8Array(32)))
|
|
155
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
156
|
+
.join("")
|
|
157
|
+
await McpAuth.updateOAuthState(this.mcpName, newState)
|
|
158
|
+
return newState
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async invalidateCredentials(type: "all" | "client" | "tokens"): Promise<void> {
|
|
162
|
+
log.info("invalidating credentials", { mcpName: this.mcpName, type })
|
|
163
|
+
const entry = await McpAuth.get(this.mcpName)
|
|
164
|
+
if (!entry) {
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
switch (type) {
|
|
169
|
+
case "all":
|
|
170
|
+
await McpAuth.remove(this.mcpName)
|
|
171
|
+
break
|
|
172
|
+
case "client":
|
|
173
|
+
delete entry.clientInfo
|
|
174
|
+
await McpAuth.set(this.mcpName, entry)
|
|
175
|
+
break
|
|
176
|
+
case "tokens":
|
|
177
|
+
delete entry.tokens
|
|
178
|
+
await McpAuth.set(this.mcpName, entry)
|
|
179
|
+
break
|
|
148
180
|
}
|
|
149
|
-
return entry.oauthState
|
|
150
181
|
}
|
|
151
182
|
}
|
|
152
183
|
|
package/src/project/project.ts
CHANGED
|
@@ -79,7 +79,7 @@ export namespace Project {
|
|
|
79
79
|
|
|
80
80
|
// generate id from root commit
|
|
81
81
|
if (!id) {
|
|
82
|
-
const roots = await $`git rev-list --max-parents=0
|
|
82
|
+
const roots = await $`git rev-list --max-parents=0 HEAD`
|
|
83
83
|
.quiet()
|
|
84
84
|
.nothrow()
|
|
85
85
|
.cwd(sandbox)
|
|
@@ -104,6 +104,7 @@ export namespace Project {
|
|
|
104
104
|
|
|
105
105
|
id = roots[0]
|
|
106
106
|
if (id) {
|
|
107
|
+
// Write to .git dir so the cache is shared across worktrees.
|
|
107
108
|
void Bun.file(path.join(git, "opencode"))
|
|
108
109
|
.write(id)
|
|
109
110
|
.catch(() => undefined)
|
|
@@ -187,9 +188,6 @@ export namespace Project {
|
|
|
187
188
|
updated: Date.now(),
|
|
188
189
|
},
|
|
189
190
|
}
|
|
190
|
-
if (id !== "global") {
|
|
191
|
-
await migrateFromGlobal(id, worktree)
|
|
192
|
-
}
|
|
193
191
|
}
|
|
194
192
|
|
|
195
193
|
// migrate old projects before sandboxes
|
|
@@ -209,6 +207,12 @@ export namespace Project {
|
|
|
209
207
|
if (sandbox !== result.worktree && !result.sandboxes.includes(sandbox)) result.sandboxes.push(sandbox)
|
|
210
208
|
result.sandboxes = result.sandboxes.filter((x) => existsSync(x))
|
|
211
209
|
await Storage.write<Info>(["project", id], result)
|
|
210
|
+
// Runs after write so the target project record exists.
|
|
211
|
+
// Runs on every startup because sessions created before git init
|
|
212
|
+
// accumulate under "global" and need migrating whenever they appear.
|
|
213
|
+
if (id !== "global") {
|
|
214
|
+
await migrateFromGlobal(id, worktree)
|
|
215
|
+
}
|
|
212
216
|
GlobalBus.emit("event", {
|
|
213
217
|
payload: {
|
|
214
218
|
type: Event.Updated.type,
|
package/src/provider/models.ts
CHANGED
package/src/provider/provider.ts
CHANGED
|
@@ -39,6 +39,8 @@ import { createVercel } from "@ai-sdk/vercel"
|
|
|
39
39
|
import { createGitLab } from "@gitlab/gitlab-ai-provider"
|
|
40
40
|
import { ProviderTransform } from "./transform"
|
|
41
41
|
|
|
42
|
+
const DEFAULT_CHUNK_TIMEOUT = 300_000
|
|
43
|
+
|
|
42
44
|
export namespace Provider {
|
|
43
45
|
const log = Log.create({ service: "provider" })
|
|
44
46
|
|
|
@@ -54,6 +56,78 @@ export namespace Provider {
|
|
|
54
56
|
return isGpt5OrLater(modelID) && !modelID.startsWith("gpt-5-mini")
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
function googleVertexVars(options: Record<string, any>) {
|
|
60
|
+
const project =
|
|
61
|
+
options["project"] ?? Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT")
|
|
62
|
+
const location =
|
|
63
|
+
options["location"] ?? Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "us-central1"
|
|
64
|
+
const endpoint = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
GOOGLE_VERTEX_PROJECT: project,
|
|
68
|
+
GOOGLE_VERTEX_LOCATION: location,
|
|
69
|
+
GOOGLE_VERTEX_ENDPOINT: endpoint,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function loadBaseURL(model: Model, options: Record<string, any>) {
|
|
74
|
+
const raw = options["baseURL"] ?? model.api.url
|
|
75
|
+
if (typeof raw !== "string") return raw
|
|
76
|
+
const vars = model.providerID === "google-vertex" ? googleVertexVars(options) : undefined
|
|
77
|
+
return raw.replace(/\$\{([^}]+)\}/g, (match, key) => {
|
|
78
|
+
const val = Env.get(String(key)) ?? vars?.[String(key) as keyof typeof vars]
|
|
79
|
+
return val ?? match
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function wrapSSE(res: Response, ms: number, ctl: AbortController) {
|
|
84
|
+
if (typeof ms !== "number" || ms <= 0) return res
|
|
85
|
+
if (!res.body) return res
|
|
86
|
+
if (!res.headers.get("content-type")?.includes("text/event-stream")) return res
|
|
87
|
+
|
|
88
|
+
const reader = res.body.getReader()
|
|
89
|
+
const body = new ReadableStream<Uint8Array>({
|
|
90
|
+
async pull(ctrl) {
|
|
91
|
+
const part = await new Promise<Awaited<ReturnType<typeof reader.read>>>((resolve, reject) => {
|
|
92
|
+
const id = setTimeout(() => {
|
|
93
|
+
const err = new Error("SSE read timed out")
|
|
94
|
+
ctl.abort(err)
|
|
95
|
+
void reader.cancel(err)
|
|
96
|
+
reject(err)
|
|
97
|
+
}, ms)
|
|
98
|
+
|
|
99
|
+
reader.read().then(
|
|
100
|
+
(part) => {
|
|
101
|
+
clearTimeout(id)
|
|
102
|
+
resolve(part)
|
|
103
|
+
},
|
|
104
|
+
(err) => {
|
|
105
|
+
clearTimeout(id)
|
|
106
|
+
reject(err)
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
if (part.done) {
|
|
112
|
+
ctrl.close()
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
ctrl.enqueue(part.value)
|
|
117
|
+
},
|
|
118
|
+
async cancel(reason) {
|
|
119
|
+
ctl.abort(reason)
|
|
120
|
+
await reader.cancel(reason)
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
return new Response(body, {
|
|
125
|
+
headers: new Headers(res.headers),
|
|
126
|
+
status: res.status,
|
|
127
|
+
statusText: res.statusText,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
57
131
|
const BUNDLED_PROVIDERS: Record<string, (options: any) => SDK> = {
|
|
58
132
|
"@ai-sdk/amazon-bedrock": createAmazonBedrock,
|
|
59
133
|
"@ai-sdk/anthropic": createAnthropic,
|
|
@@ -1402,21 +1476,23 @@ export namespace Provider {
|
|
|
1402
1476
|
}
|
|
1403
1477
|
|
|
1404
1478
|
const customFetch = options["fetch"]
|
|
1479
|
+
const chunkTimeout = options["chunkTimeout"] || DEFAULT_CHUNK_TIMEOUT
|
|
1480
|
+
delete options["chunkTimeout"]
|
|
1405
1481
|
|
|
1406
1482
|
options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
|
|
1407
1483
|
// Preserve custom fetch if it exists, wrap it with timeout logic
|
|
1408
1484
|
const fetchFn = customFetch ?? fetch
|
|
1409
1485
|
const opts = init ?? {}
|
|
1486
|
+
const chunkAbortCtl = typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined
|
|
1487
|
+
const signals: AbortSignal[] = []
|
|
1410
1488
|
|
|
1411
|
-
if (
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
const combined = signals.length > 1 ? AbortSignal.any(signals) : signals[0]
|
|
1489
|
+
if (opts.signal) signals.push(opts.signal)
|
|
1490
|
+
if (chunkAbortCtl) signals.push(chunkAbortCtl.signal)
|
|
1491
|
+
if (options["timeout"] !== undefined && options["timeout"] !== null && options["timeout"] !== false)
|
|
1492
|
+
signals.push(AbortSignal.timeout(options["timeout"]))
|
|
1417
1493
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1494
|
+
const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals)
|
|
1495
|
+
if (combined) opts.signal = combined
|
|
1420
1496
|
|
|
1421
1497
|
// Strip openai itemId metadata following what codex does
|
|
1422
1498
|
// Codex uses #[serde(skip_serializing)] on id fields for all item types:
|
|
@@ -1436,11 +1512,14 @@ export namespace Provider {
|
|
|
1436
1512
|
}
|
|
1437
1513
|
}
|
|
1438
1514
|
|
|
1439
|
-
|
|
1515
|
+
const res = await fetchFn(input, {
|
|
1440
1516
|
...opts,
|
|
1441
1517
|
// @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
|
|
1442
1518
|
timeout: false,
|
|
1443
1519
|
})
|
|
1520
|
+
|
|
1521
|
+
if (!chunkAbortCtl) return res
|
|
1522
|
+
return wrapSSE(res, chunkTimeout, chunkAbortCtl)
|
|
1444
1523
|
}
|
|
1445
1524
|
|
|
1446
1525
|
// Special case: google-vertex-anthropic uses a subpath import
|
|
@@ -46,7 +46,7 @@ export namespace ProviderTransform {
|
|
|
46
46
|
): ModelMessage[] {
|
|
47
47
|
// Anthropic rejects messages with empty content - filter out empty string messages
|
|
48
48
|
// and remove empty text/reasoning parts from array content
|
|
49
|
-
if (model.api.npm === "@ai-sdk/anthropic") {
|
|
49
|
+
if (model.api.npm === "@ai-sdk/anthropic" || model.api.npm === "@ai-sdk/amazon-bedrock") {
|
|
50
50
|
msgs = msgs
|
|
51
51
|
.map((msg) => {
|
|
52
52
|
if (typeof msg.content === "string") {
|
|
@@ -492,8 +492,13 @@ export namespace ProviderTransform {
|
|
|
492
492
|
},
|
|
493
493
|
}
|
|
494
494
|
}
|
|
495
|
+
let levels = ["low", "high"]
|
|
496
|
+
if (id.includes("3.1")) {
|
|
497
|
+
levels = ["low", "medium", "high"]
|
|
498
|
+
}
|
|
499
|
+
|
|
495
500
|
return Object.fromEntries(
|
|
496
|
-
|
|
501
|
+
levels.map((effort) => [
|
|
497
502
|
effort,
|
|
498
503
|
{
|
|
499
504
|
includeThoughts: true,
|
|
@@ -38,20 +38,33 @@ export const toolDefinition: MCPToolDefinition = {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export async function execute(args: any, context: ServiceNowContext): Promise<ToolResult> {
|
|
41
|
-
const
|
|
41
|
+
const PLUGIN_HINT =
|
|
42
|
+
"The rm_team table does not exist. Activate the 'Agile Development 2.0 - Team component' plugin (com.snc.sdlc.agile.2.0.team) to enable team management."
|
|
43
|
+
|
|
42
44
|
try {
|
|
43
45
|
const client = await getAuthenticatedClient(context)
|
|
44
46
|
|
|
47
|
+
// Verify rm_team table exists
|
|
48
|
+
try {
|
|
49
|
+
await client.get("/api/now/table/rm_team", { params: { sysparm_limit: 0 } })
|
|
50
|
+
} catch (e: any) {
|
|
51
|
+
const msg = (e.message || "") + (e.response?.data?.error?.message || "")
|
|
52
|
+
if (msg.indexOf("Invalid table") !== -1 || msg.indexOf("ACL") !== -1) {
|
|
53
|
+
return createErrorResult(PLUGIN_HINT)
|
|
54
|
+
}
|
|
55
|
+
throw e
|
|
56
|
+
}
|
|
57
|
+
|
|
45
58
|
// Resolve team
|
|
46
59
|
const teamResp = await client.get("/api/now/table/rm_team", {
|
|
47
60
|
params: {
|
|
48
|
-
sysparm_query: "name=" + team + "^ORsys_id=" + team,
|
|
61
|
+
sysparm_query: "name=" + args.team + "^ORsys_id=" + args.team,
|
|
49
62
|
sysparm_limit: 1,
|
|
50
63
|
sysparm_display_value: "true",
|
|
51
64
|
},
|
|
52
65
|
})
|
|
53
66
|
const teams = teamResp.data.result || []
|
|
54
|
-
if (teams.length === 0) return createErrorResult("Team not found: " + team)
|
|
67
|
+
if (teams.length === 0) return createErrorResult("Team not found: " + args.team)
|
|
55
68
|
const teamRecord = teams[0]
|
|
56
69
|
|
|
57
70
|
// Get team members
|
|
@@ -66,9 +79,9 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
66
79
|
|
|
67
80
|
// Resolve target sprint
|
|
68
81
|
var targetSprint: any = null
|
|
69
|
-
if (sprint) {
|
|
70
|
-
var sprintQuery = "sys_id=" + sprint
|
|
71
|
-
if (sprint.indexOf("SPRNT") === 0) sprintQuery = "number=" + sprint
|
|
82
|
+
if (args.sprint) {
|
|
83
|
+
var sprintQuery = "sys_id=" + args.sprint
|
|
84
|
+
if (args.sprint.indexOf("SPRNT") === 0) sprintQuery = "number=" + args.sprint
|
|
72
85
|
const sprintResp = await client.get("/api/now/table/rm_sprint", {
|
|
73
86
|
params: { sysparm_query: sprintQuery, sysparm_limit: 1, sysparm_display_value: "true" },
|
|
74
87
|
})
|
|
@@ -91,7 +104,7 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
91
104
|
const closedSprintsResp = await client.get("/api/now/table/rm_sprint", {
|
|
92
105
|
params: {
|
|
93
106
|
sysparm_query: "group=" + teamRecord.sys_id + "^state=3^ORDERBYDESCend_date",
|
|
94
|
-
sysparm_limit: velocity_sprints,
|
|
107
|
+
sysparm_limit: args.velocity_sprints || 3,
|
|
95
108
|
sysparm_fields: "sys_id,number,story_points",
|
|
96
109
|
},
|
|
97
110
|
})
|
|
@@ -132,12 +132,7 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
132
132
|
if (teamId) {
|
|
133
133
|
const prevSprintsResp = await client.get("/api/now/table/rm_sprint", {
|
|
134
134
|
params: {
|
|
135
|
-
sysparm_query:
|
|
136
|
-
"group=" +
|
|
137
|
-
teamId +
|
|
138
|
-
"^state=3^sys_id!=" +
|
|
139
|
-
sprintId +
|
|
140
|
-
"^ORDERBYDESCend_date",
|
|
135
|
+
sysparm_query: "group=" + teamId + "^state=3^sys_id!=" + sprintId + "^ORDERBYDESCend_date",
|
|
141
136
|
sysparm_limit: compare_sprints,
|
|
142
137
|
sysparm_fields: "sys_id,short_description,number,story_points,start_date,end_date",
|
|
143
138
|
sysparm_display_value: "true",
|
|
@@ -150,6 +145,7 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
150
145
|
params: {
|
|
151
146
|
sysparm_query: "sprint=" + prevSprints[j].sys_id,
|
|
152
147
|
sysparm_fields: "story_points,state",
|
|
148
|
+
sysparm_display_value: "true",
|
|
153
149
|
},
|
|
154
150
|
})
|
|
155
151
|
const prevStories = prevStoriesResp.data.result || []
|
|
@@ -159,7 +155,7 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
159
155
|
for (var k = 0; k < prevStories.length; k++) {
|
|
160
156
|
var prevPts = parseInt(prevStories[k].story_points, 10) || 0
|
|
161
157
|
prevPlanned += prevPts
|
|
162
|
-
if (prevStories[k].state === "
|
|
158
|
+
if (prevStories[k].state === "Closed Complete") prevCompleted += prevPts
|
|
163
159
|
}
|
|
164
160
|
|
|
165
161
|
comparison.push({
|
|
@@ -237,7 +233,9 @@ function generateInsights(
|
|
|
237
233
|
var recent = comparison[0]
|
|
238
234
|
var previous = comparison[1]
|
|
239
235
|
if (rate > recent.completion_rate)
|
|
240
|
-
insights.push(
|
|
236
|
+
insights.push(
|
|
237
|
+
"Velocity improving compared to previous sprint (" + recent.completion_rate + "% -> " + rate + "%).",
|
|
238
|
+
)
|
|
241
239
|
else if (rate < recent.completion_rate)
|
|
242
240
|
insights.push(
|
|
243
241
|
"Velocity declining compared to previous sprint (" + recent.completion_rate + "% -> " + rate + "%).",
|
|
@@ -59,24 +59,36 @@ export const toolDefinition: MCPToolDefinition = {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export async function execute(args: any, context: ServiceNowContext): Promise<ToolResult> {
|
|
62
|
-
const { action, sys_id, name, start_date, end_date, team, story_points, goal } = args
|
|
63
62
|
try {
|
|
64
63
|
const client = await getAuthenticatedClient(context)
|
|
65
64
|
|
|
66
|
-
if (action === "create") {
|
|
67
|
-
if (!name) return createErrorResult("name is required for create action")
|
|
68
|
-
const body: Record<string, any> = { short_description: name }
|
|
69
|
-
if (start_date) body.start_date = start_date
|
|
70
|
-
if (end_date) body.end_date = end_date
|
|
71
|
-
if (story_points) body.story_points = story_points
|
|
72
|
-
if (goal) body.goal = goal
|
|
65
|
+
if (args.action === "create") {
|
|
66
|
+
if (!args.name) return createErrorResult("name is required for create action")
|
|
67
|
+
const body: Record<string, any> = { short_description: args.name }
|
|
68
|
+
if (args.start_date) body.start_date = args.start_date
|
|
69
|
+
if (args.end_date) body.end_date = args.end_date
|
|
70
|
+
if (args.story_points) body.story_points = args.story_points
|
|
71
|
+
if (args.goal) body.goal = args.goal
|
|
73
72
|
|
|
74
|
-
if (team) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
if (args.team) {
|
|
74
|
+
try {
|
|
75
|
+
const teamResp = await client.get("/api/now/table/rm_team", {
|
|
76
|
+
params: {
|
|
77
|
+
sysparm_query: "name=" + args.team + "^ORsys_id=" + args.team,
|
|
78
|
+
sysparm_limit: 1,
|
|
79
|
+
sysparm_fields: "sys_id",
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
const teams = teamResp.data.result || []
|
|
83
|
+
if (teams.length > 0) body.assignment_group = teams[0].sys_id
|
|
84
|
+
} catch (_e: any) {
|
|
85
|
+
const msg = _e.message || ""
|
|
86
|
+
if (msg.indexOf("Invalid table") !== -1) {
|
|
87
|
+
return createErrorResult(
|
|
88
|
+
"The rm_team table is not available. Activate the Agile Development 2.0 plugin (com.snc.sdlc.agile.2.0) to enable team management. You can still create sprints without a team by omitting the 'team' parameter.",
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
const response = await client.post("/api/now/table/rm_sprint", body)
|
|
@@ -86,32 +98,38 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
86
98
|
})
|
|
87
99
|
}
|
|
88
100
|
|
|
89
|
-
if (!sys_id) return createErrorResult("sys_id is required for " + action + " action")
|
|
101
|
+
if (!args.sys_id) return createErrorResult("sys_id is required for " + args.action + " action")
|
|
90
102
|
|
|
91
|
-
if (action === "start") {
|
|
92
|
-
const response = await client.patch("/api/now/table/rm_sprint/" + sys_id, { state: "2" })
|
|
103
|
+
if (args.action === "start") {
|
|
104
|
+
const response = await client.patch("/api/now/table/rm_sprint/" + args.sys_id, { state: "2" })
|
|
93
105
|
return createSuccessResult({ action: "started", sprint: response.data.result })
|
|
94
106
|
}
|
|
95
107
|
|
|
96
|
-
if (action === "close") {
|
|
97
|
-
const response = await client.patch("/api/now/table/rm_sprint/" + sys_id, { state: "3" })
|
|
108
|
+
if (args.action === "close") {
|
|
109
|
+
const response = await client.patch("/api/now/table/rm_sprint/" + args.sys_id, { state: "3" })
|
|
98
110
|
return createSuccessResult({ action: "closed", sprint: response.data.result })
|
|
99
111
|
}
|
|
100
112
|
|
|
101
|
-
if (action === "update") {
|
|
113
|
+
if (args.action === "update") {
|
|
102
114
|
const updates: Record<string, any> = {}
|
|
103
|
-
if (name) updates.short_description = name
|
|
104
|
-
if (start_date) updates.start_date = start_date
|
|
105
|
-
if (end_date) updates.end_date = end_date
|
|
106
|
-
if (story_points) updates.story_points = story_points
|
|
107
|
-
if (goal) updates.goal = goal
|
|
108
|
-
const response = await client.patch("/api/now/table/rm_sprint/" + sys_id, updates)
|
|
115
|
+
if (args.name) updates.short_description = args.name
|
|
116
|
+
if (args.start_date) updates.start_date = args.start_date
|
|
117
|
+
if (args.end_date) updates.end_date = args.end_date
|
|
118
|
+
if (args.story_points) updates.story_points = args.story_points
|
|
119
|
+
if (args.goal) updates.goal = args.goal
|
|
120
|
+
const response = await client.patch("/api/now/table/rm_sprint/" + args.sys_id, updates)
|
|
109
121
|
return createSuccessResult({ action: "updated", sprint: response.data.result })
|
|
110
122
|
}
|
|
111
123
|
|
|
112
|
-
return createErrorResult("Unknown action: " + action)
|
|
124
|
+
return createErrorResult("Unknown action: " + args.action)
|
|
113
125
|
} catch (error: any) {
|
|
114
|
-
|
|
126
|
+
const msg = error.message || "Operation failed"
|
|
127
|
+
if (msg.indexOf("Invalid table") !== -1) {
|
|
128
|
+
return createErrorResult(
|
|
129
|
+
"The rm_sprint table is not available. Activate the Agile Development 2.0 plugin (com.snc.sdlc.agile.2.0) on your instance.",
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
return createErrorResult("Sprint " + args.action + " failed: " + msg)
|
|
115
133
|
}
|
|
116
134
|
}
|
|
117
135
|
|
|
@@ -55,43 +55,66 @@ export const toolDefinition: MCPToolDefinition = {
|
|
|
55
55
|
},
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
async function resolveUser(client: any, username: string): Promise<string | null> {
|
|
59
|
+
const resp = await client.get("/api/now/table/sys_user", {
|
|
60
|
+
params: {
|
|
61
|
+
sysparm_query: "user_name=" + username + "^ORsys_id=" + username,
|
|
62
|
+
sysparm_limit: 1,
|
|
63
|
+
sysparm_fields: "sys_id",
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
const users = resp.data.result || []
|
|
67
|
+
return users.length > 0 ? users[0].sys_id : null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const PLUGIN_HINT =
|
|
71
|
+
"The rm_team table is not available. Activate the Agile Development 2.0 plugin (com.snc.sdlc.agile.2.0) to enable team management."
|
|
72
|
+
|
|
58
73
|
export async function execute(args: any, context: ServiceNowContext): Promise<ToolResult> {
|
|
59
|
-
const { action, sys_id, name, description, velocity, member, role } = args
|
|
60
74
|
try {
|
|
61
75
|
const client = await getAuthenticatedClient(context)
|
|
62
76
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (
|
|
77
|
+
try {
|
|
78
|
+
await client.get("/api/now/table/rm_team", { params: { sysparm_limit: 1, sysparm_fields: "sys_id" } })
|
|
79
|
+
} catch (_e: any) {
|
|
80
|
+
const msg = _e.message || ""
|
|
81
|
+
if (msg.indexOf("Invalid table") !== -1 || (_e.response && _e.response.status === 404)) {
|
|
82
|
+
return createErrorResult(PLUGIN_HINT)
|
|
83
|
+
}
|
|
84
|
+
throw _e
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (args.action === "create") {
|
|
88
|
+
if (!args.name) return createErrorResult("name is required for create action")
|
|
89
|
+
const body: Record<string, any> = { name: args.name }
|
|
90
|
+
if (args.description) body.description = args.description
|
|
91
|
+
if (args.velocity) body.velocity = args.velocity
|
|
68
92
|
const response = await client.post("/api/now/table/rm_team", body)
|
|
69
93
|
return createSuccessResult({ action: "created", team: response.data.result })
|
|
70
94
|
}
|
|
71
95
|
|
|
72
|
-
if (!sys_id) return createErrorResult("sys_id is required for " + action + " action")
|
|
96
|
+
if (!args.sys_id) return createErrorResult("sys_id is required for " + args.action + " action")
|
|
73
97
|
|
|
74
|
-
if (action === "update") {
|
|
98
|
+
if (args.action === "update") {
|
|
75
99
|
const body: Record<string, any> = {}
|
|
76
|
-
if (name) body.name = name
|
|
77
|
-
if (description) body.description = description
|
|
78
|
-
if (velocity) body.velocity = velocity
|
|
79
|
-
const response = await client.patch("/api/now/table/rm_team/" + sys_id, body)
|
|
100
|
+
if (args.name) body.name = args.name
|
|
101
|
+
if (args.description) body.description = args.description
|
|
102
|
+
if (args.velocity) body.velocity = args.velocity
|
|
103
|
+
const response = await client.patch("/api/now/table/rm_team/" + args.sys_id, body)
|
|
80
104
|
return createSuccessResult({ action: "updated", team: response.data.result })
|
|
81
105
|
}
|
|
82
106
|
|
|
83
|
-
if (action === "list_members") {
|
|
107
|
+
if (args.action === "list_members") {
|
|
84
108
|
const membersResp = await client.get("/api/now/table/rm_team_member", {
|
|
85
109
|
params: {
|
|
86
|
-
sysparm_query: "team=" + sys_id,
|
|
110
|
+
sysparm_query: "team=" + args.sys_id,
|
|
87
111
|
sysparm_fields: "sys_id,user,role,allocation",
|
|
88
112
|
sysparm_display_value: "true",
|
|
89
113
|
},
|
|
90
114
|
})
|
|
91
115
|
const members = membersResp.data.result || []
|
|
92
116
|
|
|
93
|
-
|
|
94
|
-
const teamResp = await client.get("/api/now/table/rm_team/" + sys_id, {
|
|
117
|
+
const teamResp = await client.get("/api/now/table/rm_team/" + args.sys_id, {
|
|
95
118
|
params: { sysparm_display_value: "true" },
|
|
96
119
|
})
|
|
97
120
|
|
|
@@ -102,37 +125,24 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
102
125
|
})
|
|
103
126
|
}
|
|
104
127
|
|
|
105
|
-
if (action === "add_member") {
|
|
106
|
-
if (!member) return createErrorResult("member is required for add_member action")
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (member.indexOf("SYS") !== 0 && member.length !== 32) {
|
|
111
|
-
const userLookup = await client.get("/api/now/table/sys_user", {
|
|
112
|
-
params: {
|
|
113
|
-
sysparm_query: "user_name=" + member + "^ORsys_id=" + member,
|
|
114
|
-
sysparm_limit: 1,
|
|
115
|
-
sysparm_fields: "sys_id,user_name,name",
|
|
116
|
-
},
|
|
117
|
-
})
|
|
118
|
-
const users = userLookup.data.result || []
|
|
119
|
-
if (users.length === 0) return createErrorResult("User not found: " + member)
|
|
120
|
-
userId = users[0].sys_id
|
|
121
|
-
}
|
|
128
|
+
if (args.action === "add_member") {
|
|
129
|
+
if (!args.member) return createErrorResult("member is required for add_member action")
|
|
130
|
+
|
|
131
|
+
const userId = args.member.length === 32 ? args.member : await resolveUser(client, args.member)
|
|
132
|
+
if (!userId) return createErrorResult("User not found: " + args.member)
|
|
122
133
|
|
|
123
|
-
const body: Record<string, any> = { team: sys_id, user: userId }
|
|
124
|
-
if (role) body.role = role
|
|
134
|
+
const body: Record<string, any> = { team: args.sys_id, user: userId }
|
|
135
|
+
if (args.role) body.role = args.role
|
|
125
136
|
const response = await client.post("/api/now/table/rm_team_member", body)
|
|
126
137
|
return createSuccessResult({ action: "member_added", member: response.data.result })
|
|
127
138
|
}
|
|
128
139
|
|
|
129
|
-
if (action === "remove_member") {
|
|
130
|
-
if (!member) return createErrorResult("member is required for remove_member action")
|
|
140
|
+
if (args.action === "remove_member") {
|
|
141
|
+
if (!args.member) return createErrorResult("member is required for remove_member action")
|
|
131
142
|
|
|
132
|
-
// Find the team_member record
|
|
133
143
|
const memberLookup = await client.get("/api/now/table/rm_team_member", {
|
|
134
144
|
params: {
|
|
135
|
-
sysparm_query: "team=" + sys_id + "^user=" + member + "^ORuser.user_name=" + member,
|
|
145
|
+
sysparm_query: "team=" + args.sys_id + "^user=" + args.member + "^ORuser.user_name=" + args.member,
|
|
136
146
|
sysparm_limit: 1,
|
|
137
147
|
sysparm_fields: "sys_id",
|
|
138
148
|
},
|
|
@@ -144,9 +154,11 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
144
154
|
return createSuccessResult({ action: "member_removed", member_sys_id: found[0].sys_id })
|
|
145
155
|
}
|
|
146
156
|
|
|
147
|
-
return createErrorResult("Unknown action: " + action)
|
|
157
|
+
return createErrorResult("Unknown action: " + args.action)
|
|
148
158
|
} catch (error: any) {
|
|
149
|
-
|
|
159
|
+
const msg = error.message || "Operation failed"
|
|
160
|
+
if (msg.indexOf("Invalid table") !== -1) return createErrorResult(PLUGIN_HINT)
|
|
161
|
+
return createErrorResult("Team " + args.action + " failed: " + msg)
|
|
150
162
|
}
|
|
151
163
|
}
|
|
152
164
|
|
|
@@ -56,7 +56,14 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
56
56
|
})
|
|
57
57
|
|
|
58
58
|
const sprints = sprintsResp.data.result || []
|
|
59
|
-
if (sprints.length === 0)
|
|
59
|
+
if (sprints.length === 0)
|
|
60
|
+
return createSuccessResult({
|
|
61
|
+
message:
|
|
62
|
+
"No sprints found for team: " +
|
|
63
|
+
args.team +
|
|
64
|
+
". If using Agile 2.0 teams, ensure the 'Agile Development 2.0 - Team component' plugin (com.snc.sdlc.agile.2.0.team) is activated and sprints are linked to the team's group.",
|
|
65
|
+
velocity: [],
|
|
66
|
+
})
|
|
60
67
|
|
|
61
68
|
var velocityData: any[] = []
|
|
62
69
|
var totalCompleted = 0
|