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.
Files changed (62) hide show
  1. package/bin/index.js.map +9 -9
  2. package/bin/worker.js.map +7 -7
  3. package/mcp/servicenow-unified.js +116 -116
  4. package/package.json +1 -1
  5. package/parsers-config.ts +2 -1
  6. package/src/bun/index.ts +10 -9
  7. package/src/cli/cmd/agent.ts +3 -3
  8. package/src/cli/cmd/auth.ts +46 -0
  9. package/src/cli/cmd/import.ts +2 -2
  10. package/src/cli/cmd/session.ts +9 -12
  11. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +2 -1
  12. package/src/cli/cmd/tui/component/prompt/index.tsx +19 -6
  13. package/src/cli/cmd/tui/component/spinner.tsx +24 -0
  14. package/src/cli/cmd/tui/context/exit.tsx +1 -1
  15. package/src/cli/cmd/tui/routes/home.tsx +16 -2
  16. package/src/cli/cmd/tui/routes/session/index.tsx +122 -53
  17. package/src/cli/cmd/tui/routes/session/permission.tsx +9 -1
  18. package/src/cli/cmd/tui/routes/session/sidebar.tsx +9 -1
  19. package/src/cli/cmd/tui/thread.ts +4 -1
  20. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +1 -1
  21. package/src/cli/cmd/tui/util/clipboard.ts +3 -3
  22. package/src/cli/cmd/tui/worker.ts +6 -1
  23. package/src/config/config.ts +28 -0
  24. package/src/context/context-db.ts +437 -0
  25. package/src/format/formatter.ts +14 -5
  26. package/src/global/index.ts +3 -4
  27. package/src/mcp/index.ts +7 -2
  28. package/src/mcp/oauth-callback.ts +7 -15
  29. package/src/mcp/oauth-provider.ts +34 -3
  30. package/src/project/project.ts +8 -4
  31. package/src/provider/models.ts +1 -1
  32. package/src/provider/provider.ts +88 -9
  33. package/src/provider/transform.ts +7 -2
  34. package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_capacity_plan.ts +20 -7
  35. package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_retrospective.ts +6 -8
  36. package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_sprint_manage.ts +46 -28
  37. package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_team_manage.ts +53 -41
  38. package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_velocity_report.ts +8 -1
  39. package/src/servicenow/servicenow-mcp-unified/tools/automation/snow_schedule_script_job.ts +388 -243
  40. package/src/session/compaction.ts +126 -23
  41. package/src/session/message-v2.ts +33 -10
  42. package/src/session/processor.ts +29 -17
  43. package/src/session/prompt.ts +34 -6
  44. package/src/share/share-next.ts +2 -2
  45. package/src/shell/shell.ts +2 -1
  46. package/src/tool/edit.ts +15 -1
  47. package/src/tool/registry.ts +9 -1
  48. package/src/tool/truncation.ts +17 -0
  49. package/src/tool/websearch.ts +1 -1
  50. package/src/tool/websearch.txt +2 -2
  51. package/src/tool/write.ts +3 -4
  52. package/src/util/filesystem.ts +36 -7
  53. package/src/util/keybind.ts +1 -1
  54. package/src/util/log.ts +8 -5
  55. package/src/util/token.ts +28 -0
  56. package/test/cli/plugin-auth-picker.test.ts +120 -0
  57. package/test/fixture/fixture.ts +3 -0
  58. package/test/mcp/oauth-auto-connect.test.ts +197 -0
  59. package/test/project/project.test.ts +47 -0
  60. package/test/provider/provider.test.ts +2 -0
  61. package/test/provider/transform.test.ts +32 -0
  62. 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 (!entry?.oauthState) {
147
- throw new Error(`No OAuth state saved for MCP server: ${this.mcpName}`)
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
 
@@ -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 --all`
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,
@@ -122,7 +122,7 @@ export namespace ModelsDev {
122
122
  }
123
123
  }
124
124
 
125
- if (!Flag.OPENCODE_DISABLE_MODELS_FETCH) {
125
+ if (!Flag.OPENCODE_DISABLE_MODELS_FETCH && !process.argv.includes("--get-yargs-completions")) {
126
126
  ModelsDev.refresh()
127
127
  setInterval(
128
128
  async () => {
@@ -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 (options["timeout"] !== undefined && options["timeout"] !== null) {
1412
- const signals: AbortSignal[] = []
1413
- if (opts.signal) signals.push(opts.signal)
1414
- if (options["timeout"] !== false) signals.push(AbortSignal.timeout(options["timeout"]))
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
- opts.signal = combined
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
- return fetchFn(input, {
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
- ["low", "high"].map((effort) => [
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 { team, sprint, velocity_sprints = 3 } = args
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 === "3") prevCompleted += prevPts // state value for Closed Complete
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("Velocity improving compared to previous sprint (" + recent.completion_rate + "% -> " + rate + "%).")
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
- const teamResp = await client.get("/api/now/table/rm_team", {
76
- params: { sysparm_query: "name=" + team + "^ORsys_id=" + team, sysparm_limit: 1, sysparm_fields: "sys_id" },
77
- })
78
- const teams = teamResp.data.result || []
79
- if (teams.length > 0) body.group = teams[0].sys_id
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
- return createErrorResult(error.message)
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
- if (action === "create") {
64
- if (!name) return createErrorResult("name is required for create action")
65
- const body: Record<string, any> = { name }
66
- if (description) body.description = description
67
- if (velocity) body.velocity = velocity
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
- // Also get the team details
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
- // Resolve user
109
- var userId = member
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
- return createErrorResult(error.message)
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) return createSuccessResult({ message: "No sprints found for team: " + team, velocity: [] })
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