mcp-for-i 0.1.13 → 0.2.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/README.md +17 -0
- package/dist/config/store.js +229 -109
- package/dist/controlplane/auth.js +128 -0
- package/dist/controlplane/clientIntegrationService.js +83 -4
- package/dist/controlplane/commands.js +14 -9
- package/dist/controlplane/connectionService.js +110 -5
- package/dist/controlplane/connectionTest.js +329 -0
- package/dist/controlplane/errors.js +12 -0
- package/dist/controlplane/runtimeService.js +98 -26
- package/dist/controlplane/server.js +122 -11
- package/dist/controlplane/serverShared.js +25 -6
- package/dist/controlplane/serviceManager.js +113 -100
- package/dist/controlplane/ui.js +1742 -920
- package/dist/ibmi/CompileTools.js +29 -1
- package/dist/ibmi/IBMiContent.js +435 -63
- package/dist/ibmi/IbmDocs.js +685 -0
- package/dist/ibmi/LocalLanguageActions.js +129 -3
- package/dist/ibmi/Search.js +158 -52
- package/dist/ibmi/Tools.js +7 -10
- package/dist/ibmi/client.js +220 -31
- package/dist/ibmi/evfevent.js +256 -0
- package/dist/ibmi/knownHosts.js +190 -0
- package/dist/ibmi/libraryList.js +93 -0
- package/dist/ibmi/sourceInspect.js +815 -0
- package/dist/ibmi/sqlJob.js +888 -0
- package/dist/ibmi/sqlParse.js +317 -0
- package/dist/ibmi/tn5250/certTrust.js +59 -0
- package/dist/ibmi/tn5250/client.js +310 -0
- package/dist/ibmi/tn5250/datastream.js +109 -0
- package/dist/ibmi/tn5250/ebcdic.js +74 -0
- package/dist/ibmi/tn5250/index.js +6 -0
- package/dist/ibmi/tn5250/screen.js +827 -0
- package/dist/ibmi/tn5250/telnet.js +186 -0
- package/dist/ibmi/variables.js +18 -7
- package/dist/index.js +43 -22
- package/dist/mcp/annotations.js +155 -0
- package/dist/mcp/audit.js +490 -133
- package/dist/mcp/confirm.js +48 -0
- package/dist/mcp/context.js +43 -5
- package/dist/mcp/deployMethods.js +192 -0
- package/dist/mcp/logger.js +5 -10
- package/dist/mcp/modules/bulkMembers.js +476 -0
- package/dist/mcp/modules/changedLines.js +189 -0
- package/dist/mcp/modules/compare.js +198 -0
- package/dist/mcp/modules/compile.js +515 -0
- package/dist/mcp/modules/ifsOps.js +123 -0
- package/dist/mcp/modules/index.js +38 -0
- package/dist/mcp/modules/inspect.js +811 -0
- package/dist/mcp/modules/libl.js +246 -0
- package/dist/mcp/modules/members.js +171 -0
- package/dist/mcp/modules/objects.js +339 -0
- package/dist/mcp/modules/system.js +266 -0
- package/dist/mcp/modules/tn5250.js +515 -0
- package/dist/mcp/policy.js +412 -0
- package/dist/mcp/tools.js +1700 -698
- package/dist/security/credentialStore.js +62 -13
- package/dist/security/localPaths.js +138 -0
- package/dist/security/redact.js +79 -0
- package/dist/util/atomicWrite.js +101 -0
- package/dist/util/fileLock.js +206 -0
- package/docs/capabilities.md +50 -7
- package/docs/configuration.md +189 -60
- package/docs/examples.md +109 -73
- package/docs/overview.md +15 -15
- package/docs/security.md +101 -16
- package/docs/tools.md +363 -188
- package/package.json +56 -53
package/dist/mcp/tools.js
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
2
|
+
import { findToolModule, notifyModulesOfDisconnect, toolModules } from "./modules/index.js";
|
|
3
|
+
import { withAnnotations } from "./annotations.js";
|
|
4
|
+
import { runActionTool } from "./modules/compile.js";
|
|
5
|
+
import { parseEvfevent } from "../ibmi/evfevent.js";
|
|
1
6
|
import { LocalLanguageActions } from "../ibmi/LocalLanguageActions.js";
|
|
2
|
-
import { CompileTools } from "../ibmi/CompileTools.js";
|
|
3
7
|
import { Search } from "../ibmi/Search.js";
|
|
8
|
+
import { ibmDocs } from "../ibmi/IbmDocs.js";
|
|
9
|
+
import { detectSourceLanguage, inspectSource } from "../ibmi/sourceInspect.js";
|
|
4
10
|
import { Tools } from "../ibmi/Tools.js";
|
|
11
|
+
import { checkReadOnlySql, getSqlStatementType as parseSqlStatementType } from "../ibmi/sqlParse.js";
|
|
5
12
|
import { ConnectionService } from "../controlplane/connectionService.js";
|
|
6
13
|
import { log } from "./logger.js";
|
|
14
|
+
import { assertAgentConnectionChangeAllowed, assertSafeIfsDeletePath, checkClCommand, checkClCommands, checkResourcePolicy, checkShellCommand, checkSqlEmbeddedCl, clStartsShell, isShellOperation } from "./policy.js";
|
|
15
|
+
import { configuredLocalRoots, resolveAllowedLocalPath } from "../security/localPaths.js";
|
|
7
16
|
import { exportAuditRecords, listAuditRecords, purgeAuditRecords, verifyAuditChain } from "./audit.js";
|
|
17
|
+
import { gitChangedFiles, normalizeDeployMethod, normalizeDeploySelection, sha256LocalFile, sha256RemoteFiles } from "./deployMethods.js";
|
|
8
18
|
import fs from "fs/promises";
|
|
9
19
|
import path from "path";
|
|
10
20
|
import crypto from "crypto";
|
|
@@ -39,7 +49,14 @@ const SETTINGS_SCHEMA = {
|
|
|
39
49
|
autoClearTempData: { type: "boolean" },
|
|
40
50
|
sourceFileCCSID: { type: "string" },
|
|
41
51
|
sqlJobCcsid: { type: ["string", "number"] },
|
|
52
|
+
sqlTransport: { type: "string", enum: ["auto", "mapepire", "qzdfmdb2"] },
|
|
53
|
+
mapepireMode: { type: "string", enum: ["auto", "daemon", "ssh"] },
|
|
54
|
+
mapepirePort: { type: "number" },
|
|
55
|
+
mapepireServerPath: { type: "string" },
|
|
56
|
+
mapepireJavaPath: { type: "string" },
|
|
42
57
|
enableSourceDates: { type: "boolean" },
|
|
58
|
+
sourceReadMaxBytes: { type: "number" },
|
|
59
|
+
sourceReadMaxLines: { type: "number" },
|
|
43
60
|
homeDirectory: { type: "string" },
|
|
44
61
|
libraryList: { type: "array", items: { type: "string" } },
|
|
45
62
|
currentLibrary: { type: "string" },
|
|
@@ -50,20 +67,10 @@ const SETTINGS_SCHEMA = {
|
|
|
50
67
|
debugSepPort: { type: "number" },
|
|
51
68
|
sessionIdleMinutes: { type: "number" },
|
|
52
69
|
sessionPingSeconds: { type: "number" },
|
|
53
|
-
sessionReconnectAttempts: { type: "number" }
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
type: "object",
|
|
58
|
-
properties: {
|
|
59
|
-
profile: { type: "string", enum: ["read-only", "guarded", "power-user"] },
|
|
60
|
-
requireApprovalFor: { type: "array", items: { type: "string" } },
|
|
61
|
-
allowCommands: { type: "array", items: { type: "string" } },
|
|
62
|
-
denyCommands: { type: "array", items: { type: "string" } },
|
|
63
|
-
allowLibraries: { type: "array", items: { type: "string" } },
|
|
64
|
-
denyLibraries: { type: "array", items: { type: "string" } },
|
|
65
|
-
allowPaths: { type: "array", items: { type: "string" } },
|
|
66
|
-
denyPaths: { type: "array", items: { type: "string" } }
|
|
70
|
+
sessionReconnectAttempts: { type: "number" },
|
|
71
|
+
tn5250Port: { type: "number", description: "TN5250 telnet port (default 23, or 992 when tn5250Tls is true)." },
|
|
72
|
+
tn5250Tls: { type: "boolean", description: "Use TLS for TN5250 sessions (default true when the port is 992)." },
|
|
73
|
+
tn5250DeviceName: { type: "string", description: "Virtual device name requested for TN5250 sessions." }
|
|
67
74
|
}
|
|
68
75
|
};
|
|
69
76
|
const ACTION_SCHEMA = {
|
|
@@ -87,13 +94,32 @@ const PROFILE_SCHEMA = {
|
|
|
87
94
|
name: { type: "string" },
|
|
88
95
|
currentLibrary: { type: "string" },
|
|
89
96
|
libraryList: { type: "array", items: { type: "string" } },
|
|
90
|
-
customVariables: { type: "array", items: CUSTOM_VARIABLE_SCHEMA }
|
|
97
|
+
customVariables: { type: "array", items: CUSTOM_VARIABLE_SCHEMA },
|
|
98
|
+
setLibraryListCommand: { type: "string", description: "CL command run by ibmi.libl.runProfileCommand to set the library list." }
|
|
91
99
|
},
|
|
92
100
|
required: ["name"]
|
|
93
101
|
};
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
102
|
+
const LINE_RANGE_SCHEMA = {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: {
|
|
105
|
+
start: { type: "number" },
|
|
106
|
+
end: { type: "number" }
|
|
107
|
+
},
|
|
108
|
+
required: ["start", "end"]
|
|
109
|
+
};
|
|
110
|
+
const SOURCE_INSPECTION_OPERATION_SCHEMA = {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: {
|
|
113
|
+
operation: { type: "string", enum: ["metadata", "outline", "search", "read_ranges", "dependencies"] },
|
|
114
|
+
query: { type: "string" },
|
|
115
|
+
terms: { type: "array", items: { type: "string" } },
|
|
116
|
+
ranges: { type: "array", items: LINE_RANGE_SCHEMA },
|
|
117
|
+
before: { type: "number" },
|
|
118
|
+
after: { type: "number" },
|
|
119
|
+
maxMatches: { type: "number" }
|
|
120
|
+
},
|
|
121
|
+
required: ["operation"]
|
|
122
|
+
};
|
|
97
123
|
export function getTools() {
|
|
98
124
|
const tools = [
|
|
99
125
|
{
|
|
@@ -133,6 +159,15 @@ export function getTools() {
|
|
|
133
159
|
description: "Terminate a session.",
|
|
134
160
|
inputSchema: { type: "object", properties: { connectionName: { type: "string" } } }
|
|
135
161
|
},
|
|
162
|
+
{
|
|
163
|
+
name: "ibmi.session.bootstrap",
|
|
164
|
+
description: "Return connection, IBM i release, library-list, policy, and transport capabilities in one call.",
|
|
165
|
+
inputSchema: {
|
|
166
|
+
type: "object",
|
|
167
|
+
properties: { connectionName: { type: "string" } },
|
|
168
|
+
required: ["connectionName"]
|
|
169
|
+
}
|
|
170
|
+
},
|
|
136
171
|
{
|
|
137
172
|
name: "ibmi.connections.list",
|
|
138
173
|
description: "List saved IBM i connections.",
|
|
@@ -140,7 +175,7 @@ export function getTools() {
|
|
|
140
175
|
},
|
|
141
176
|
{
|
|
142
177
|
name: "ibmi.connections.add",
|
|
143
|
-
description: "Add a saved IBM i connection (credentials are managed via local control-plane UI).",
|
|
178
|
+
description: "Add a saved IBM i connection (credentials are managed via local control-plane UI). The policy and the safety settings (readOnlyMode, tempDir, tempLibrary, autoClearTempData, mapepire*) can only be set in the web UI or config.json.",
|
|
144
179
|
inputSchema: {
|
|
145
180
|
type: "object",
|
|
146
181
|
properties: {
|
|
@@ -149,8 +184,7 @@ export function getTools() {
|
|
|
149
184
|
port: { type: "number", default: 22 },
|
|
150
185
|
username: { type: "string" },
|
|
151
186
|
privateKeyPath: { type: "string" },
|
|
152
|
-
settings: SETTINGS_SCHEMA
|
|
153
|
-
policy: POLICY_SCHEMA
|
|
187
|
+
settings: SETTINGS_SCHEMA
|
|
154
188
|
},
|
|
155
189
|
required: ["name", "host", "username"],
|
|
156
190
|
additionalProperties: false
|
|
@@ -158,7 +192,7 @@ export function getTools() {
|
|
|
158
192
|
},
|
|
159
193
|
{
|
|
160
194
|
name: "ibmi.connections.update",
|
|
161
|
-
description: "Update a saved IBM i connection (credentials are managed via local control-plane UI).",
|
|
195
|
+
description: "Update a saved IBM i connection (credentials are managed via local control-plane UI). The policy and the safety settings (readOnlyMode, tempDir, tempLibrary, autoClearTempData, mapepire*) can only be changed in the web UI or config.json.",
|
|
162
196
|
inputSchema: {
|
|
163
197
|
type: "object",
|
|
164
198
|
properties: {
|
|
@@ -167,8 +201,7 @@ export function getTools() {
|
|
|
167
201
|
port: { type: "number" },
|
|
168
202
|
username: { type: "string" },
|
|
169
203
|
privateKeyPath: { type: "string" },
|
|
170
|
-
settings: SETTINGS_SCHEMA
|
|
171
|
-
policy: POLICY_SCHEMA
|
|
204
|
+
settings: SETTINGS_SCHEMA
|
|
172
205
|
},
|
|
173
206
|
required: ["name"],
|
|
174
207
|
additionalProperties: false
|
|
@@ -214,25 +247,56 @@ export function getTools() {
|
|
|
214
247
|
description: "List members in a source file.",
|
|
215
248
|
inputSchema: {
|
|
216
249
|
type: "object",
|
|
217
|
-
properties: {
|
|
250
|
+
properties: {
|
|
251
|
+
connectionName: { type: "string" },
|
|
252
|
+
library: { type: "string" },
|
|
253
|
+
sourceFile: { type: "string" },
|
|
254
|
+
member: { type: "string" },
|
|
255
|
+
memberPrefix: { type: "string" },
|
|
256
|
+
memberContains: { type: "string" },
|
|
257
|
+
sourceTypes: { type: "array", items: { type: "string" } },
|
|
258
|
+
changedSince: { type: "string" },
|
|
259
|
+
limit: { type: "number" },
|
|
260
|
+
cursor: { type: "string" },
|
|
261
|
+
responseFormat: { type: "string", enum: ["legacy-array", "page"] }
|
|
262
|
+
},
|
|
218
263
|
required: ["library", "sourceFile"]
|
|
219
264
|
}
|
|
220
265
|
},
|
|
221
266
|
{
|
|
222
267
|
name: "ibmi.qsys.members.read",
|
|
223
|
-
description: "Read a member.",
|
|
268
|
+
description: "Read a member. Full reads larger than the sourceReadMaxBytes/sourceReadMaxLines settings fail with SOURCE_TOO_LARGE; use startLine/endLine, ranges, ibmi.search.members or ibmi.source.inspect instead. startLine without endLine reads to the end of the member.",
|
|
224
269
|
inputSchema: {
|
|
225
270
|
type: "object",
|
|
226
|
-
properties: {
|
|
271
|
+
properties: {
|
|
272
|
+
connectionName: { type: "string" },
|
|
273
|
+
library: { type: "string" },
|
|
274
|
+
sourceFile: { type: "string" },
|
|
275
|
+
member: { type: "string" },
|
|
276
|
+
startLine: { type: "number" },
|
|
277
|
+
endLine: { type: "number" },
|
|
278
|
+
ranges: { type: "array", items: LINE_RANGE_SCHEMA },
|
|
279
|
+
before: { type: "number" },
|
|
280
|
+
after: { type: "number" },
|
|
281
|
+
metadataOnly: { type: "boolean" },
|
|
282
|
+
expectedVersion: { type: "string" },
|
|
283
|
+
expectedSha256: { type: "string" },
|
|
284
|
+
forceFresh: { type: "boolean" },
|
|
285
|
+
responseFormat: { type: "string", enum: ["text", "structured"] }
|
|
286
|
+
},
|
|
227
287
|
required: ["library", "sourceFile", "member"]
|
|
228
288
|
}
|
|
229
289
|
},
|
|
230
290
|
{
|
|
231
291
|
name: "ibmi.qsys.members.write",
|
|
232
|
-
description: "Write a member.",
|
|
292
|
+
description: "Write a member. Pass expectedVersion or expectedSha256 from a structured read to reject the write (VERSION_CONFLICT error) if the member changed. With a version check or responseFormat \"structured\" the result carries the new version and sha256 for chaining edits.",
|
|
233
293
|
inputSchema: {
|
|
234
294
|
type: "object",
|
|
235
|
-
properties: {
|
|
295
|
+
properties: {
|
|
296
|
+
connectionName: { type: "string" }, library: { type: "string" }, sourceFile: { type: "string" }, member: { type: "string" }, content: { type: "string" },
|
|
297
|
+
expectedVersion: { type: "string" }, expectedSha256: { type: "string" },
|
|
298
|
+
responseFormat: { type: "string", enum: ["text", "structured"] }
|
|
299
|
+
},
|
|
236
300
|
required: ["library", "sourceFile", "member", "content"]
|
|
237
301
|
}
|
|
238
302
|
},
|
|
@@ -284,8 +348,16 @@ export function getTools() {
|
|
|
284
348
|
},
|
|
285
349
|
{
|
|
286
350
|
name: "ibmi.ifs.read",
|
|
287
|
-
description: "Read an IFS file.",
|
|
288
|
-
inputSchema: {
|
|
351
|
+
description: "Read an IFS file. Text is returned as-is; files that are not valid UTF-8 text are returned base64-encoded. Files larger than maxBytes are refused (use ibmi.ifs.download).",
|
|
352
|
+
inputSchema: {
|
|
353
|
+
type: "object",
|
|
354
|
+
properties: {
|
|
355
|
+
path: { type: "string" },
|
|
356
|
+
maxBytes: { type: "number", description: "Maximum file size to read (default 1048576, max 16777216)." },
|
|
357
|
+
encoding: { type: "string", enum: ["auto", "utf8", "base64"], default: "auto" }
|
|
358
|
+
},
|
|
359
|
+
required: ["path"]
|
|
360
|
+
}
|
|
289
361
|
},
|
|
290
362
|
{
|
|
291
363
|
name: "ibmi.ifs.write",
|
|
@@ -299,8 +371,16 @@ export function getTools() {
|
|
|
299
371
|
},
|
|
300
372
|
{
|
|
301
373
|
name: "ibmi.ifs.delete",
|
|
302
|
-
description: "Delete an IFS file or directory.",
|
|
303
|
-
inputSchema: {
|
|
374
|
+
description: "Delete an IFS file or directory. A recursive delete needs confirmPath equal to path and is refused for paths less than three levels deep (for example /home/USER).",
|
|
375
|
+
inputSchema: {
|
|
376
|
+
type: "object",
|
|
377
|
+
properties: {
|
|
378
|
+
path: { type: "string" },
|
|
379
|
+
recursive: { type: "boolean" },
|
|
380
|
+
confirmPath: { type: "string", description: "Required with recursive=true: repeat the exact path to confirm the delete." }
|
|
381
|
+
},
|
|
382
|
+
required: ["path"]
|
|
383
|
+
}
|
|
304
384
|
},
|
|
305
385
|
{
|
|
306
386
|
name: "ibmi.ifs.upload",
|
|
@@ -314,16 +394,93 @@ export function getTools() {
|
|
|
314
394
|
},
|
|
315
395
|
{
|
|
316
396
|
name: "ibmi.search.members",
|
|
317
|
-
description: "Search in source members.",
|
|
397
|
+
description: "Search in source members. Returns at most maxMatches matching lines (default 100, maximum 1000) and reports truncated: true when more exist. Lines are marked kind \"match\" or \"context\".",
|
|
318
398
|
inputSchema: {
|
|
319
399
|
type: "object",
|
|
320
400
|
properties: {
|
|
401
|
+
connectionName: { type: "string" },
|
|
321
402
|
library: { type: "string" },
|
|
322
403
|
sourceFile: { type: "string" },
|
|
323
404
|
term: { type: "string" },
|
|
324
|
-
|
|
405
|
+
terms: { type: "array", items: { type: "string" } },
|
|
406
|
+
members: { type: ["string", "array"], items: { type: "string" } },
|
|
407
|
+
caseSensitive: { type: "boolean" },
|
|
408
|
+
before: { type: "number" },
|
|
409
|
+
after: { type: "number" },
|
|
410
|
+
maxMatches: { type: "number" }
|
|
411
|
+
},
|
|
412
|
+
required: ["library", "sourceFile"]
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
name: "ibmi.source.resolve",
|
|
417
|
+
description: "Resolve an exact source member across candidate source files without listing every member.",
|
|
418
|
+
inputSchema: {
|
|
419
|
+
type: "object",
|
|
420
|
+
properties: {
|
|
421
|
+
connectionName: { type: "string" },
|
|
422
|
+
library: { type: "string" },
|
|
423
|
+
member: { type: "string" },
|
|
424
|
+
candidateSourceFiles: { type: "array", items: { type: "string" } },
|
|
425
|
+
sourceTypes: { type: "array", items: { type: "string" } },
|
|
426
|
+
limit: { type: "number" }
|
|
427
|
+
},
|
|
428
|
+
required: ["connectionName", "library", "member"]
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: "ibmi.source.inspect",
|
|
433
|
+
description: "Resolve and inspect one source member with batched metadata, outline, search, ranges, and dependency operations.",
|
|
434
|
+
inputSchema: {
|
|
435
|
+
type: "object",
|
|
436
|
+
properties: {
|
|
437
|
+
connectionName: { type: "string" },
|
|
438
|
+
library: { type: "string" },
|
|
439
|
+
sourceFile: { type: "string" },
|
|
440
|
+
member: { type: "string" },
|
|
441
|
+
candidateSourceFiles: { type: "array", items: { type: "string" } },
|
|
442
|
+
sourceTypes: { type: "array", items: { type: "string" } },
|
|
443
|
+
operations: { type: "array", items: SOURCE_INSPECTION_OPERATION_SCHEMA },
|
|
444
|
+
expectedVersion: { type: "string" },
|
|
445
|
+
expectedSha256: { type: "string" },
|
|
446
|
+
forceFresh: { type: "boolean" }
|
|
447
|
+
},
|
|
448
|
+
required: ["connectionName", "library", "member", "operations"]
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
name: "ibmi.docs.search",
|
|
453
|
+
description: "Search the release-aware catalog and cache of official IBM i documentation.",
|
|
454
|
+
inputSchema: {
|
|
455
|
+
type: "object",
|
|
456
|
+
properties: {
|
|
457
|
+
query: { type: "string" },
|
|
458
|
+
connectionName: { type: "string" },
|
|
459
|
+
releases: { type: "array", items: { type: "string" } },
|
|
460
|
+
categories: { type: "array", items: { type: "string" } },
|
|
461
|
+
limit: { type: "number" },
|
|
462
|
+
cursor: { type: "string" },
|
|
463
|
+
freshness: { type: "string", enum: ["indexed", "prefer-live"] }
|
|
325
464
|
},
|
|
326
|
-
required: ["
|
|
465
|
+
required: ["query"]
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
name: "ibmi.docs.open",
|
|
470
|
+
description: "Open and cache an official IBM documentation page: a ref from ibmi.docs.search or any https://www.ibm.com/docs/ URL. Cached copies expire after 7 days by default.",
|
|
471
|
+
inputSchema: {
|
|
472
|
+
type: "object",
|
|
473
|
+
properties: {
|
|
474
|
+
ref: { type: "string" },
|
|
475
|
+
url: { type: "string" },
|
|
476
|
+
expectedVersion: { type: "string" },
|
|
477
|
+
requireLive: { type: "boolean" },
|
|
478
|
+
freshness: { type: "string", enum: ["allow-cached", "require-live"] },
|
|
479
|
+
section: { type: "string" },
|
|
480
|
+
startLine: { type: "number" },
|
|
481
|
+
endLine: { type: "number" },
|
|
482
|
+
maxCharacters: { type: "number" }
|
|
483
|
+
}
|
|
327
484
|
}
|
|
328
485
|
},
|
|
329
486
|
{
|
|
@@ -343,20 +500,32 @@ export function getTools() {
|
|
|
343
500
|
},
|
|
344
501
|
{
|
|
345
502
|
name: "ibmi.actions.run",
|
|
346
|
-
description: "Run an action."
|
|
503
|
+
description: "Run an action (for example a compile) against a source member or an IFS streamfile. " +
|
|
504
|
+
"Member targets set &OPENLIB/&OPENSPF/&OPENMBR/&EXT/&NAME; streamfile targets set &FULLPATH/&RELATIVEPATH/&NAME/&EXT. " +
|
|
505
|
+
"When the command uses OPTION(*EVENTF) the EVFEVENT file is read and errors are returned grouped by file with line, column, severity and message id. " +
|
|
506
|
+
"Honours the action's deployFirst (with localWorkspace + remoteWorkspace), postDownload (to downloadTo or localWorkspace) and outputToFile. " +
|
|
507
|
+
"Set includeListing to also return the compile listing spooled file.",
|
|
347
508
|
inputSchema: {
|
|
348
509
|
type: "object",
|
|
349
510
|
properties: {
|
|
350
511
|
actionName: { type: "string" },
|
|
351
512
|
action: ACTION_SCHEMA,
|
|
352
513
|
targetType: { type: "string", enum: ["member", "streamfile", "file"] },
|
|
353
|
-
targetPath: { type: "string" },
|
|
514
|
+
targetPath: { type: "string", description: "Streamfile: absolute IFS path. Member: LIB/FILE/MEMBER[.TYPE] (or use library/sourceFile/member)." },
|
|
354
515
|
library: { type: "string" },
|
|
355
516
|
sourceFile: { type: "string" },
|
|
356
517
|
member: { type: "string" },
|
|
357
|
-
extension: { type: "string" }
|
|
518
|
+
extension: { type: "string", description: "Member type (looked up when omitted) or streamfile extension." },
|
|
519
|
+
cwd: { type: "string", description: "Remote working directory for streamfile actions (default remoteWorkspace, then the home directory)." },
|
|
520
|
+
localWorkspace: { type: "string", description: "Local project folder, used by deployFirst, postDownload and relative outputToFile paths." },
|
|
521
|
+
remoteWorkspace: { type: "string", description: "IFS folder the local workspace deploys to." },
|
|
522
|
+
downloadTo: { type: "string", description: "Local folder for postDownload outputs (default localWorkspace)." },
|
|
523
|
+
evfeventLibrary: { type: "string", description: "Override the EVFEVENT library (default: the compiled object's library)." },
|
|
524
|
+
evfeventMember: { type: "string", description: "Override the EVFEVENT member (default: the compiled object's name)." },
|
|
525
|
+
includeListing: { type: "boolean", description: "Also return the compile listing spooled file." },
|
|
526
|
+
listingMaxLines: { type: "number" }
|
|
358
527
|
},
|
|
359
|
-
required: ["targetType"
|
|
528
|
+
required: ["targetType"]
|
|
360
529
|
}
|
|
361
530
|
},
|
|
362
531
|
{
|
|
@@ -534,20 +703,30 @@ export function getTools() {
|
|
|
534
703
|
},
|
|
535
704
|
{
|
|
536
705
|
name: "ibmi.deploy.compare",
|
|
537
|
-
description: "Compare local and remote directories (by relative paths).",
|
|
538
|
-
inputSchema: {
|
|
706
|
+
description: "Compare local and remote directories (by relative paths). method \"all\" compares sizes; method \"changed\" also compares SHA-256 content hashes of files present on both sides.",
|
|
707
|
+
inputSchema: {
|
|
708
|
+
type: "object",
|
|
709
|
+
properties: {
|
|
710
|
+
localPath: { type: "string" },
|
|
711
|
+
remotePath: { type: "string" },
|
|
712
|
+
method: { type: "string", enum: ["all", "changed"], default: "all" }
|
|
713
|
+
},
|
|
714
|
+
required: ["localPath", "remotePath"]
|
|
715
|
+
}
|
|
539
716
|
},
|
|
540
717
|
{
|
|
541
718
|
name: "ibmi.deploy.sync",
|
|
542
|
-
description: "Sync local directory to
|
|
719
|
+
description: "Sync a local directory to the IFS. method: \"all\" (default) uploads missing files and, with overwrite, size-changed ones; \"changed\" uploads only files whose SHA-256 differs or that are missing remotely; \"gitChanged\" uploads staged, unstaged and untracked files from git status; \"selection\" uploads the listed files (paths relative to localPath). Reports uploaded, unchanged and skipped counts.",
|
|
543
720
|
inputSchema: {
|
|
544
721
|
type: "object",
|
|
545
722
|
properties: {
|
|
546
723
|
localPath: { type: "string" },
|
|
547
724
|
remotePath: { type: "string" },
|
|
548
|
-
|
|
725
|
+
method: { type: "string", enum: ["all", "changed", "gitChanged", "selection"], default: "all" },
|
|
726
|
+
files: { type: "array", items: { type: "string" }, description: "Relative file paths for method \"selection\"." },
|
|
727
|
+
overwrite: { type: "boolean", default: false, description: "method \"all\" only: also upload files whose size differs." },
|
|
549
728
|
dryRun: { type: "boolean", default: false },
|
|
550
|
-
deleteExtraRemote: { type: "boolean", default: false }
|
|
729
|
+
deleteExtraRemote: { type: "boolean", default: false, description: "methods \"all\" and \"changed\" only." }
|
|
551
730
|
},
|
|
552
731
|
required: ["localPath", "remotePath"]
|
|
553
732
|
}
|
|
@@ -600,7 +779,7 @@ export function getTools() {
|
|
|
600
779
|
},
|
|
601
780
|
{
|
|
602
781
|
name: "ibmi.diagnostics.parseEvfevent",
|
|
603
|
-
description: "Parse EVFEVENT
|
|
782
|
+
description: "Parse EVFEVENT (compiler events file) records into errors grouped by source file, with line, column, severity and message id. Errors in SQL precompiler temp files are mapped back to the original source.",
|
|
604
783
|
inputSchema: { type: "object", properties: { content: { type: "string" } }, required: ["content"] }
|
|
605
784
|
},
|
|
606
785
|
{
|
|
@@ -610,8 +789,18 @@ export function getTools() {
|
|
|
610
789
|
},
|
|
611
790
|
{
|
|
612
791
|
name: "ibmi.spool.list",
|
|
613
|
-
description: "List spool file entries.",
|
|
614
|
-
inputSchema: {
|
|
792
|
+
description: "List spool file entries, newest first, optionally filtered by user, output queue or job.",
|
|
793
|
+
inputSchema: {
|
|
794
|
+
type: "object",
|
|
795
|
+
properties: {
|
|
796
|
+
connectionName: { type: "string" },
|
|
797
|
+
limit: { type: "number" },
|
|
798
|
+
userName: { type: "string", description: "User profile that owns the spooled files." },
|
|
799
|
+
outputQueue: { type: "string", description: "Output queue name." },
|
|
800
|
+
outputQueueLibrary: { type: "string", description: "Output queue library." },
|
|
801
|
+
jobName: { type: "string", description: "Qualified job name (number/user/name) or a simple job name." }
|
|
802
|
+
}
|
|
803
|
+
}
|
|
615
804
|
},
|
|
616
805
|
{
|
|
617
806
|
name: "ibmi.spool.read",
|
|
@@ -878,14 +1067,15 @@ export function getTools() {
|
|
|
878
1067
|
},
|
|
879
1068
|
{
|
|
880
1069
|
name: "ibmi.dataqueue.receive",
|
|
881
|
-
description: "
|
|
1070
|
+
description: "Peek at (default) or receive an entry from a data queue. remove=true takes the entry off the queue and needs a writable connection.",
|
|
882
1071
|
inputSchema: {
|
|
883
1072
|
type: "object",
|
|
884
1073
|
properties: {
|
|
885
1074
|
library: { type: "string" },
|
|
886
1075
|
queue: { type: "string" },
|
|
887
|
-
waitSeconds: { type: "number", default: 0 },
|
|
888
|
-
remove: { type: "boolean", default:
|
|
1076
|
+
waitSeconds: { type: "number", default: 0, description: "Seconds to wait for an entry (0 to 60)." },
|
|
1077
|
+
remove: { type: "boolean", default: false },
|
|
1078
|
+
approve: { type: "boolean", description: "Only used when the connection's approvalMode is \"agent\" (remove=true). By default the user is asked to confirm." },
|
|
889
1079
|
connectionName: { type: "string" }
|
|
890
1080
|
},
|
|
891
1081
|
required: ["library", "queue"]
|
|
@@ -985,7 +1175,7 @@ export function getTools() {
|
|
|
985
1175
|
},
|
|
986
1176
|
{
|
|
987
1177
|
name: "ibmi.journal.entries.query",
|
|
988
|
-
description: "Read normalized entries
|
|
1178
|
+
description: "Read normalized journal entries, newest first. Filters are applied on the IBM i before the row limit.",
|
|
989
1179
|
inputSchema: {
|
|
990
1180
|
type: "object",
|
|
991
1181
|
properties: {
|
|
@@ -993,6 +1183,7 @@ export function getTools() {
|
|
|
993
1183
|
journal: { type: "string" },
|
|
994
1184
|
objectLibrary: { type: "string" },
|
|
995
1185
|
objectName: { type: "string" },
|
|
1186
|
+
objectType: { type: "string", description: "Object type for the object filter (default *FILE)." },
|
|
996
1187
|
journalCode: { type: "string" },
|
|
997
1188
|
entryType: { type: "string" },
|
|
998
1189
|
userName: { type: "string" },
|
|
@@ -1158,57 +1349,25 @@ export function getTools() {
|
|
|
1158
1349
|
connectionName: { type: "string" }
|
|
1159
1350
|
}
|
|
1160
1351
|
}
|
|
1161
|
-
},
|
|
1162
|
-
{
|
|
1163
|
-
name: "ibmi.tn5250.connect",
|
|
1164
|
-
description: "Connect to a TN5250 command session.",
|
|
1165
|
-
inputSchema: { type: "object", properties: { connectionName: { type: "string" } } }
|
|
1166
|
-
},
|
|
1167
|
-
{
|
|
1168
|
-
name: "ibmi.tn5250.readScreen",
|
|
1169
|
-
description: "Read TN5250 screen model.",
|
|
1170
|
-
inputSchema: { type: "object", properties: { connectionName: { type: "string" } } }
|
|
1171
|
-
},
|
|
1172
|
-
{
|
|
1173
|
-
name: "ibmi.tn5250.setField",
|
|
1174
|
-
description: "Set TN5250 field value.",
|
|
1175
|
-
inputSchema: {
|
|
1176
|
-
type: "object",
|
|
1177
|
-
properties: { fieldId: { type: "string" }, value: { type: "string" }, connectionName: { type: "string" } },
|
|
1178
|
-
required: ["fieldId", "value"]
|
|
1179
|
-
}
|
|
1180
|
-
},
|
|
1181
|
-
{
|
|
1182
|
-
name: "ibmi.tn5250.sendKeys",
|
|
1183
|
-
description: "Send TN5250 keys.",
|
|
1184
|
-
inputSchema: {
|
|
1185
|
-
type: "object",
|
|
1186
|
-
properties: { keys: { type: "string" }, timeoutMs: { type: "number" }, connectionName: { type: "string" }, approve: { type: "boolean" } },
|
|
1187
|
-
required: ["keys"]
|
|
1188
|
-
}
|
|
1189
|
-
},
|
|
1190
|
-
{
|
|
1191
|
-
name: "ibmi.tn5250.waitFor",
|
|
1192
|
-
description: "Wait for TN5250 text condition.",
|
|
1193
|
-
inputSchema: { type: "object", properties: { text: { type: "string" }, timeoutMs: { type: "number" }, connectionName: { type: "string" } } }
|
|
1194
|
-
},
|
|
1195
|
-
{
|
|
1196
|
-
name: "ibmi.tn5250.snapshot",
|
|
1197
|
-
description: "Capture TN5250 snapshot.",
|
|
1198
|
-
inputSchema: { type: "object", properties: { connectionName: { type: "string" } } }
|
|
1199
|
-
},
|
|
1200
|
-
{
|
|
1201
|
-
name: "ibmi.tn5250.disconnect",
|
|
1202
|
-
description: "Disconnect TN5250 session.",
|
|
1203
|
-
inputSchema: { type: "object", properties: { connectionName: { type: "string" } } }
|
|
1204
1352
|
}
|
|
1205
1353
|
];
|
|
1206
|
-
return tools
|
|
1354
|
+
return [...tools, ...toolModules.flatMap(module => module.tools)]
|
|
1207
1355
|
.map(withGuardedApprovalHint)
|
|
1208
|
-
.map(
|
|
1356
|
+
.map(withSourceConnectionName)
|
|
1357
|
+
.map(withStrictInputSchema)
|
|
1358
|
+
.map(withAnnotations);
|
|
1209
1359
|
}
|
|
1360
|
+
/** The outermost tool call being handled, so confirmation prompts can name it. */
|
|
1361
|
+
const toolCallStore = new AsyncLocalStorage();
|
|
1210
1362
|
export async function handleTool(ctx, name, args) {
|
|
1363
|
+
if (!toolCallStore.getStore()) {
|
|
1364
|
+
return toolCallStore.run({ toolName: name }, () => handleTool(ctx, name, args));
|
|
1365
|
+
}
|
|
1211
1366
|
const connections = new ConnectionService(ctx.store);
|
|
1367
|
+
await enforceResourcePolicy(ctx, name, args);
|
|
1368
|
+
const toolModule = findToolModule(name);
|
|
1369
|
+
if (toolModule)
|
|
1370
|
+
return toolModule.handle(ctx, name, args);
|
|
1212
1371
|
switch (name) {
|
|
1213
1372
|
case "ibmi.connect": {
|
|
1214
1373
|
if (!args?.name)
|
|
@@ -1222,12 +1381,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
1222
1381
|
case "ibmi.disconnect": {
|
|
1223
1382
|
const name = args?.connectionName || ctx.activeName;
|
|
1224
1383
|
await ctx.disconnect(args?.connectionName);
|
|
1225
|
-
|
|
1226
|
-
tn5250Sessions.delete(name);
|
|
1227
|
-
}
|
|
1228
|
-
else {
|
|
1229
|
-
tn5250Sessions.clear();
|
|
1230
|
-
}
|
|
1384
|
+
notifyModulesOfDisconnect(name);
|
|
1231
1385
|
return result("Disconnected");
|
|
1232
1386
|
}
|
|
1233
1387
|
case "ibmi.session.list": {
|
|
@@ -1242,21 +1396,57 @@ export async function handleTool(ctx, name, args) {
|
|
|
1242
1396
|
case "ibmi.session.terminate": {
|
|
1243
1397
|
const name = args?.connectionName || ctx.activeName;
|
|
1244
1398
|
await ctx.terminateSession(args?.connectionName);
|
|
1245
|
-
|
|
1246
|
-
tn5250Sessions.delete(name);
|
|
1399
|
+
notifyModulesOfDisconnect(name);
|
|
1247
1400
|
return result("Terminated");
|
|
1248
1401
|
}
|
|
1402
|
+
case "ibmi.session.bootstrap": {
|
|
1403
|
+
const conn = await ctx.ensureActive(args.connectionName);
|
|
1404
|
+
const system = await readSystemFacts(conn);
|
|
1405
|
+
const config = conn.getConfig();
|
|
1406
|
+
return json({
|
|
1407
|
+
connectionName: conn.currentConnectionName,
|
|
1408
|
+
host: conn.currentHost,
|
|
1409
|
+
systemName: system.systemName,
|
|
1410
|
+
effectiveUser: system.effectiveUser || conn.currentUser,
|
|
1411
|
+
release: system.release,
|
|
1412
|
+
currentLibrary: config.currentLibrary || "QGPL",
|
|
1413
|
+
libraryList: config.libraryList || [],
|
|
1414
|
+
defaultUserLibraries: conn.defaultUserLibraries,
|
|
1415
|
+
ccsid: config.sqlJobCcsid ?? 1208,
|
|
1416
|
+
sourceFileCcsid: config.sourceFileCCSID || "*FILE",
|
|
1417
|
+
policy: ctx.getPolicy(args.connectionName),
|
|
1418
|
+
transports: {
|
|
1419
|
+
sql: conn.sqlJob?.isOpen?.() ? "MAPEPIRE" : "QZDFMDB2",
|
|
1420
|
+
sqlJob: conn.sqlJob?.status?.(),
|
|
1421
|
+
sourceRead: ["sql-member", "cpytostmf"]
|
|
1422
|
+
},
|
|
1423
|
+
remoteFeatures: conn.remoteFeatures,
|
|
1424
|
+
capabilities: {
|
|
1425
|
+
version: "1",
|
|
1426
|
+
memberPaging: true,
|
|
1427
|
+
rangedMemberRead: true,
|
|
1428
|
+
multiTermSearch: true,
|
|
1429
|
+
sourceResolve: true,
|
|
1430
|
+
sourceInspect: true,
|
|
1431
|
+
optimisticConcurrency: true,
|
|
1432
|
+
ibmDocs: true
|
|
1433
|
+
},
|
|
1434
|
+
validatedAt: new Date().toISOString()
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1249
1437
|
case "ibmi.connections.list": {
|
|
1250
1438
|
const list = await connections.list();
|
|
1251
1439
|
return json(list);
|
|
1252
1440
|
}
|
|
1253
1441
|
case "ibmi.connections.add": {
|
|
1254
1442
|
rejectCredentialArgs(args, "ibmi.connections.add");
|
|
1443
|
+
assertAgentConnectionChangeAllowed("ibmi.connections.add", args);
|
|
1255
1444
|
await connections.add(args);
|
|
1256
1445
|
return result(`Connection ${args.name} saved`);
|
|
1257
1446
|
}
|
|
1258
1447
|
case "ibmi.connections.update": {
|
|
1259
1448
|
rejectCredentialArgs(args, "ibmi.connections.update");
|
|
1449
|
+
assertAgentConnectionChangeAllowed("ibmi.connections.update", args);
|
|
1260
1450
|
await connections.update(args.name, args);
|
|
1261
1451
|
return result(`Connection ${args.name} updated`);
|
|
1262
1452
|
}
|
|
@@ -1308,47 +1498,100 @@ export async function handleTool(ctx, name, args) {
|
|
|
1308
1498
|
}
|
|
1309
1499
|
case "ibmi.qsys.members.list": {
|
|
1310
1500
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1311
|
-
const data =
|
|
1501
|
+
const data = hasMemberListExtensions(args)
|
|
1502
|
+
? await conn.content.getMemberListPage({
|
|
1503
|
+
library: args.library,
|
|
1504
|
+
sourceFile: args.sourceFile,
|
|
1505
|
+
member: args.member,
|
|
1506
|
+
memberPrefix: args.memberPrefix,
|
|
1507
|
+
memberContains: args.memberContains,
|
|
1508
|
+
sourceTypes: args.sourceTypes,
|
|
1509
|
+
changedSince: args.changedSince,
|
|
1510
|
+
limit: args.limit,
|
|
1511
|
+
cursor: args.cursor
|
|
1512
|
+
})
|
|
1513
|
+
: await conn.content.getMemberList({ library: args.library, sourceFile: args.sourceFile });
|
|
1312
1514
|
return json(data);
|
|
1313
1515
|
}
|
|
1314
1516
|
case "ibmi.qsys.members.read": {
|
|
1315
1517
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1316
|
-
const
|
|
1317
|
-
|
|
1518
|
+
const limits = sourceReadLimits(conn);
|
|
1519
|
+
if (!hasStructuredMemberReadOptions(args)) {
|
|
1520
|
+
const content = await conn.content.downloadMemberContent(args.library, args.sourceFile, args.member);
|
|
1521
|
+
const tooLarge = sourceTooLarge(content, undefined, limits, args);
|
|
1522
|
+
if (tooLarge)
|
|
1523
|
+
return tooLarge;
|
|
1524
|
+
return result(content);
|
|
1525
|
+
}
|
|
1526
|
+
const wantsVersionCheck = Boolean(args.expectedVersion || args.expectedSha256);
|
|
1527
|
+
if (args.metadataOnly && !wantsVersionCheck && !args.forceFresh && typeof conn.content.getMemberVersionInfo === "function") {
|
|
1528
|
+
// Metadata-only reads never download content; a validated cached
|
|
1529
|
+
// snapshot supplies sha256/version when one is available.
|
|
1530
|
+
const info = await conn.content.getMemberVersionInfo(args.library, args.sourceFile, args.member);
|
|
1531
|
+
if (info.snapshot)
|
|
1532
|
+
return json(formatMemberRead(info.snapshot, args, limits));
|
|
1533
|
+
return json(memberMetadataEnvelope(args, info.metadata, info.observedAt));
|
|
1534
|
+
}
|
|
1535
|
+
if (isFullMemberRead(args) && typeof conn.content.getMemberMetadata === "function") {
|
|
1536
|
+
const metadata = await conn.content.getMemberMetadata(args.library, args.sourceFile, args.member);
|
|
1537
|
+
const rows = Number(metadata?.rowCount || 0);
|
|
1538
|
+
if (limits.maxLines > 0 && rows > limits.maxLines) {
|
|
1539
|
+
return sourceTooLargeError(args, { lineCount: rows, metadata }, limits);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
const snapshot = await conn.content.getMemberSnapshot(args.library, args.sourceFile, args.member, Boolean(args.forceFresh));
|
|
1543
|
+
const conflict = memberVersionConflict(snapshot, args);
|
|
1544
|
+
if (conflict)
|
|
1545
|
+
return conflict;
|
|
1546
|
+
return formatMemberReadResult(snapshot, args, limits);
|
|
1318
1547
|
}
|
|
1319
1548
|
case "ibmi.qsys.members.write": {
|
|
1320
1549
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1321
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
1550
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
1551
|
+
const versionChecked = Boolean(args.expectedVersion || args.expectedSha256);
|
|
1552
|
+
let previous;
|
|
1553
|
+
if (versionChecked) {
|
|
1554
|
+
// Re-read the member fresh immediately before writing. A change made
|
|
1555
|
+
// between this check and the write itself can still slip through;
|
|
1556
|
+
// the host offers no compare-and-swap for source members.
|
|
1557
|
+
previous = await conn.content.getMemberSnapshot(args.library, args.sourceFile, args.member, true);
|
|
1558
|
+
const conflict = memberVersionConflict(previous, args);
|
|
1559
|
+
if (conflict)
|
|
1560
|
+
return conflict;
|
|
1561
|
+
}
|
|
1322
1562
|
await conn.content.uploadMemberContent(args.library, args.sourceFile, args.member, args.content);
|
|
1323
|
-
|
|
1563
|
+
const written = await describeWrittenMember(conn, args, previous);
|
|
1564
|
+
if (versionChecked || args.responseFormat === "structured")
|
|
1565
|
+
return json(written);
|
|
1566
|
+
return { content: [{ type: "text", text: "OK" }], structuredContent: { ok: true, text: "OK", data: written } };
|
|
1324
1567
|
}
|
|
1325
1568
|
case "ibmi.qsys.members.create": {
|
|
1326
1569
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1327
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
1570
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
1328
1571
|
await conn.content.createMember(args.library, args.sourceFile, args.member, args.srctype);
|
|
1329
1572
|
return result("OK");
|
|
1330
1573
|
}
|
|
1331
1574
|
case "ibmi.qsys.members.rename": {
|
|
1332
1575
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1333
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
1576
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
1334
1577
|
await conn.content.renameMember(args.library, args.sourceFile, args.member, args.newMember);
|
|
1335
1578
|
return result("OK");
|
|
1336
1579
|
}
|
|
1337
1580
|
case "ibmi.qsys.members.delete": {
|
|
1338
1581
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1339
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
1582
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
1340
1583
|
await conn.content.deleteMember(args.library, args.sourceFile, args.member);
|
|
1341
1584
|
return result("OK");
|
|
1342
1585
|
}
|
|
1343
1586
|
case "ibmi.qsys.sourcefiles.create": {
|
|
1344
1587
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1345
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
1588
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
1346
1589
|
await conn.content.createSourceFile(args.library, args.sourceFile, args.rcdlen || 112);
|
|
1347
1590
|
return result("OK");
|
|
1348
1591
|
}
|
|
1349
1592
|
case "ibmi.qsys.libraries.create": {
|
|
1350
1593
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1351
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
1594
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
1352
1595
|
await conn.content.createLibrary(args.library);
|
|
1353
1596
|
return result("OK");
|
|
1354
1597
|
}
|
|
@@ -1359,48 +1602,159 @@ export async function handleTool(ctx, name, args) {
|
|
|
1359
1602
|
}
|
|
1360
1603
|
case "ibmi.ifs.read": {
|
|
1361
1604
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1362
|
-
const
|
|
1363
|
-
|
|
1605
|
+
const maxBytes = clampNumber(args?.maxBytes, IFS_READ_DEFAULT_MAX_BYTES, 1, IFS_READ_HARD_MAX_BYTES);
|
|
1606
|
+
const encoding = String(args?.encoding || "auto").toLowerCase();
|
|
1607
|
+
if (!["auto", "utf8", "base64"].includes(encoding))
|
|
1608
|
+
throw new Error("encoding must be auto, utf8 or base64");
|
|
1609
|
+
const knownSize = await statIfsFileSize(conn, String(args.path));
|
|
1610
|
+
if (knownSize !== undefined && knownSize > maxBytes) {
|
|
1611
|
+
return toolError(`File is ${knownSize} bytes, which is more than maxBytes (${maxBytes}). Raise maxBytes or use ibmi.ifs.download.`, { path: args.path, size: knownSize, maxBytes });
|
|
1612
|
+
}
|
|
1613
|
+
const data = Buffer.from(await conn.content.downloadStreamfileRaw(args.path));
|
|
1614
|
+
if (data.length > maxBytes) {
|
|
1615
|
+
return toolError(`File is ${data.length} bytes, which is more than maxBytes (${maxBytes}). Raise maxBytes or use ibmi.ifs.download.`, { path: args.path, size: data.length, maxBytes });
|
|
1616
|
+
}
|
|
1617
|
+
const text = encoding === "base64" ? undefined : decodeUtf8Text(data);
|
|
1618
|
+
if (text !== undefined)
|
|
1619
|
+
return result(text);
|
|
1620
|
+
if (encoding === "utf8") {
|
|
1621
|
+
return toolError("File is not valid UTF-8 text; read it with encoding=base64.", { path: args.path, size: data.length });
|
|
1622
|
+
}
|
|
1623
|
+
return json({ path: args.path, size: data.length, encoding: "base64", content: data.toString("base64") });
|
|
1364
1624
|
}
|
|
1365
1625
|
case "ibmi.ifs.write": {
|
|
1366
1626
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1367
1627
|
const op = resolveIfsWriteOperation(args.path);
|
|
1368
|
-
enforceWritable(ctx, conn, op, args);
|
|
1628
|
+
await enforceWritable(ctx, conn, op, args);
|
|
1369
1629
|
await conn.content.writeStreamfileRaw(args.path, args.content);
|
|
1370
1630
|
return result("OK");
|
|
1371
1631
|
}
|
|
1372
1632
|
case "ibmi.ifs.mkdir": {
|
|
1373
1633
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1374
1634
|
const op = resolveIfsWriteOperation(args.path);
|
|
1375
|
-
enforceWritable(ctx, conn, op, args);
|
|
1376
|
-
await conn.sendCommand({ command: `mkdir -p ${Tools.escapePath(args.path)}` });
|
|
1635
|
+
await enforceWritable(ctx, conn, op, args);
|
|
1636
|
+
requireCommandSuccess(await conn.sendCommand({ command: `mkdir -p ${Tools.escapePath(args.path)}` }), "mkdir");
|
|
1377
1637
|
return result("OK");
|
|
1378
1638
|
}
|
|
1379
1639
|
case "ibmi.ifs.delete": {
|
|
1380
1640
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1381
|
-
enforceWritable(ctx, conn, "ifs.delete", args);
|
|
1641
|
+
await enforceWritable(ctx, conn, "ifs.delete", args);
|
|
1642
|
+
const deletePath = assertSafeIfsDeletePath(args.path);
|
|
1643
|
+
if (args.recursive) {
|
|
1644
|
+
// A recursive delete removes a whole tree, so it must be deep enough and confirmed.
|
|
1645
|
+
if (deletePath.split("/").filter(Boolean).length < 3) {
|
|
1646
|
+
throw new Error(`ifs.delete refused: recursive deletes need a path at least three levels deep (${deletePath})`);
|
|
1647
|
+
}
|
|
1648
|
+
if (typeof args.confirmPath !== "string" || args.confirmPath.trim().replace(/\/+$/, "") !== deletePath) {
|
|
1649
|
+
throw new Error("ifs.delete refused: recursive deletes need confirmPath set to the exact path being deleted");
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1382
1652
|
const flag = args.recursive ? "-rf" : "-f";
|
|
1383
|
-
await conn.sendCommand({ command: `rm ${flag} ${Tools.escapePath(
|
|
1653
|
+
requireCommandSuccess(await conn.sendCommand({ command: `rm ${flag} ${Tools.escapePath(deletePath)}` }), "rm");
|
|
1384
1654
|
return result("OK");
|
|
1385
1655
|
}
|
|
1386
1656
|
case "ibmi.ifs.upload": {
|
|
1387
1657
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1388
1658
|
const op = resolveIfsWriteOperation(args.remotePath);
|
|
1389
|
-
enforceWritable(ctx, conn, op, args);
|
|
1390
|
-
await
|
|
1659
|
+
await enforceWritable(ctx, conn, op, args);
|
|
1660
|
+
const localPath = await guardLocalPath(ctx, args.localPath, "read");
|
|
1661
|
+
await conn.client.putFile(localPath, args.remotePath);
|
|
1391
1662
|
return result("OK");
|
|
1392
1663
|
}
|
|
1393
1664
|
case "ibmi.ifs.download": {
|
|
1394
1665
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1395
|
-
await
|
|
1666
|
+
const localPath = await guardLocalPath(ctx, args.localPath, "write");
|
|
1667
|
+
await conn.client.getFile(localPath, args.remotePath);
|
|
1396
1668
|
return result("OK");
|
|
1397
1669
|
}
|
|
1398
1670
|
case "ibmi.search.members": {
|
|
1399
1671
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1400
1672
|
const members = args.members || "*";
|
|
1401
|
-
const
|
|
1673
|
+
const terms = normalizeSearchTerms(args);
|
|
1674
|
+
const data = await Search.searchMembersTerms(conn, args.library, args.sourceFile, terms, members, false, {
|
|
1675
|
+
caseSensitive: Boolean(args.caseSensitive),
|
|
1676
|
+
before: args.before,
|
|
1677
|
+
after: args.after,
|
|
1678
|
+
maxMatches: args.maxMatches
|
|
1679
|
+
});
|
|
1402
1680
|
return json(data);
|
|
1403
1681
|
}
|
|
1682
|
+
case "ibmi.source.resolve": {
|
|
1683
|
+
const conn = await ctx.ensureActive(args.connectionName);
|
|
1684
|
+
const candidates = await conn.content.resolveMembers({
|
|
1685
|
+
library: args.library,
|
|
1686
|
+
member: args.member,
|
|
1687
|
+
candidateSourceFiles: args.candidateSourceFiles,
|
|
1688
|
+
sourceTypes: args.sourceTypes,
|
|
1689
|
+
limit: args.limit
|
|
1690
|
+
});
|
|
1691
|
+
rankMemberCandidates(candidates, args.candidateSourceFiles);
|
|
1692
|
+
return json({
|
|
1693
|
+
status: candidates.length === 0 ? "not-found" : candidates.length === 1 ? "found" : "ambiguous",
|
|
1694
|
+
library: String(args.library).toUpperCase(),
|
|
1695
|
+
member: String(args.member).toUpperCase(),
|
|
1696
|
+
candidates,
|
|
1697
|
+
returned: candidates.length,
|
|
1698
|
+
observedAt: new Date().toISOString()
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
case "ibmi.source.inspect": {
|
|
1702
|
+
const conn = await ctx.ensureActive(args.connectionName);
|
|
1703
|
+
const operations = Array.isArray(args.operations) ? args.operations : [];
|
|
1704
|
+
if (operations.length < 1 || operations.length > 8)
|
|
1705
|
+
throw new Error("Provide between 1 and 8 inspection operations");
|
|
1706
|
+
let sourceFile = args.sourceFile ? String(args.sourceFile).toUpperCase() : "";
|
|
1707
|
+
let resolution;
|
|
1708
|
+
if (!sourceFile) {
|
|
1709
|
+
const candidates = await conn.content.resolveMembers({
|
|
1710
|
+
library: args.library,
|
|
1711
|
+
member: args.member,
|
|
1712
|
+
candidateSourceFiles: args.candidateSourceFiles,
|
|
1713
|
+
sourceTypes: args.sourceTypes,
|
|
1714
|
+
limit: 20
|
|
1715
|
+
});
|
|
1716
|
+
rankMemberCandidates(candidates, args.candidateSourceFiles);
|
|
1717
|
+
resolution = {
|
|
1718
|
+
status: candidates.length === 0 ? "not-found" : candidates.length === 1 ? "found" : "ambiguous",
|
|
1719
|
+
candidates
|
|
1720
|
+
};
|
|
1721
|
+
if (candidates.length !== 1)
|
|
1722
|
+
return json(resolution);
|
|
1723
|
+
sourceFile = candidates[0].file;
|
|
1724
|
+
}
|
|
1725
|
+
const snapshot = await conn.content.getMemberSnapshot(args.library, sourceFile, args.member, Boolean(args.forceFresh));
|
|
1726
|
+
const conflict = memberVersionConflict(snapshot, args);
|
|
1727
|
+
if (conflict)
|
|
1728
|
+
return conflict;
|
|
1729
|
+
return json({
|
|
1730
|
+
status: "found",
|
|
1731
|
+
identity: snapshot.identity,
|
|
1732
|
+
version: snapshot.version,
|
|
1733
|
+
sha256: snapshot.sha256,
|
|
1734
|
+
observedAt: snapshot.observedAt,
|
|
1735
|
+
complete: snapshot.complete,
|
|
1736
|
+
transport: snapshot.transport,
|
|
1737
|
+
cacheStatus: snapshot.cacheStatus,
|
|
1738
|
+
warnings: snapshot.warnings,
|
|
1739
|
+
...(resolution ? { resolution } : {}),
|
|
1740
|
+
results: operations.map((operation) => inspectMemberOperation(snapshot, operation, sourceReadLimits(conn)))
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
case "ibmi.docs.search": {
|
|
1744
|
+
let releases = args.releases;
|
|
1745
|
+
if ((!releases || releases.length === 0) && args.connectionName) {
|
|
1746
|
+
const conn = await ctx.ensureActive(args.connectionName);
|
|
1747
|
+
const detected = await readSystemFacts(conn);
|
|
1748
|
+
if (detected.release)
|
|
1749
|
+
releases = [detected.release];
|
|
1750
|
+
}
|
|
1751
|
+
return json(await ibmDocs.search({ ...args, releases }));
|
|
1752
|
+
}
|
|
1753
|
+
case "ibmi.docs.open": {
|
|
1754
|
+
if (!args.ref && !args.url)
|
|
1755
|
+
throw new Error("ref or url is required");
|
|
1756
|
+
return json(await ibmDocs.open(args));
|
|
1757
|
+
}
|
|
1404
1758
|
case "ibmi.search.ifs": {
|
|
1405
1759
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1406
1760
|
const data = await Search.searchIFS(conn, args.path, args.term);
|
|
@@ -1423,49 +1777,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
1423
1777
|
return json(actions);
|
|
1424
1778
|
}
|
|
1425
1779
|
case "ibmi.actions.run": {
|
|
1426
|
-
|
|
1427
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
1428
|
-
const action = await resolveAction(ctx, args);
|
|
1429
|
-
if (!action)
|
|
1430
|
-
throw new Error("Action not found");
|
|
1431
|
-
const envVars = {};
|
|
1432
|
-
const targetPath = args.targetPath;
|
|
1433
|
-
const targetType = args.targetType;
|
|
1434
|
-
if (targetType === "file") {
|
|
1435
|
-
throw new Error("Local file actions are not supported in this MCP server (use streamfile or member).");
|
|
1436
|
-
}
|
|
1437
|
-
if (targetType === "streamfile") {
|
|
1438
|
-
envVars["&FULLPATH"] = targetPath;
|
|
1439
|
-
const base = targetPath.split("/").pop() || targetPath;
|
|
1440
|
-
envVars["&BASENAME"] = base;
|
|
1441
|
-
envVars["&NAME"] = base.includes(".") ? base.substring(0, base.lastIndexOf(".")) : base;
|
|
1442
|
-
envVars["&EXT"] = base.includes(".") ? base.substring(base.lastIndexOf(".") + 1) : "";
|
|
1443
|
-
envVars["&RELATIVEPATH"] = targetPath;
|
|
1444
|
-
}
|
|
1445
|
-
if (targetType === "member") {
|
|
1446
|
-
const lib = requireQsysName(conn, args.library, "library");
|
|
1447
|
-
const src = requireQsysName(conn, args.sourceFile, "sourceFile");
|
|
1448
|
-
const mbr = requireQsysName(conn, args.member || targetPath.split("/").pop(), "member");
|
|
1449
|
-
const ext = String(args.extension || "");
|
|
1450
|
-
envVars["&LIBRARY"] = lib;
|
|
1451
|
-
envVars["&SRCFILE"] = src;
|
|
1452
|
-
envVars["&NAME"] = mbr;
|
|
1453
|
-
envVars["&EXT"] = ext;
|
|
1454
|
-
envVars["&EXTL"] = ext.toLowerCase();
|
|
1455
|
-
envVars["&FULLPATH"] = `${lib}/${src}/${mbr}`;
|
|
1456
|
-
envVars["&OPENLIB"] = lib;
|
|
1457
|
-
envVars["&OPENLIBL"] = lib.toLowerCase();
|
|
1458
|
-
envVars["&OPENSPF"] = src;
|
|
1459
|
-
envVars["&OPENSPFL"] = src.toLowerCase();
|
|
1460
|
-
envVars["&OPENMBR"] = mbr;
|
|
1461
|
-
envVars["&OPENMBRL"] = mbr.toLowerCase();
|
|
1462
|
-
}
|
|
1463
|
-
const resultCmd = await CompileTools.runCommand(conn, {
|
|
1464
|
-
command: action.command,
|
|
1465
|
-
environment: action.environment,
|
|
1466
|
-
env: envVars
|
|
1467
|
-
});
|
|
1468
|
-
return json(mapActionExecution(action, targetType, targetPath, resultCmd));
|
|
1780
|
+
return runActionTool(ctx, args);
|
|
1469
1781
|
}
|
|
1470
1782
|
case "ibmi.actions.save": {
|
|
1471
1783
|
const action = args.action;
|
|
@@ -1499,15 +1811,13 @@ export async function handleTool(ctx, name, args) {
|
|
|
1499
1811
|
case "ibmi.libl.set": {
|
|
1500
1812
|
const name = getConnectionName(ctx, args);
|
|
1501
1813
|
const active = await ctx.ensureActive(name);
|
|
1502
|
-
enforceWritable(ctx, active, "qsys.write", args);
|
|
1814
|
+
await enforceWritable(ctx, active, "qsys.write", args);
|
|
1503
1815
|
await updateConnectionSettings(ctx, name, {
|
|
1504
1816
|
currentLibrary: args.currentLibrary,
|
|
1505
1817
|
libraryList: args.libraryList
|
|
1506
1818
|
});
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
}
|
|
1510
|
-
return result("OK");
|
|
1819
|
+
const applied = args.applyToJob ? await applyLibraryList(active, args.libraryList, args.currentLibrary) : undefined;
|
|
1820
|
+
return okApplied(applied);
|
|
1511
1821
|
}
|
|
1512
1822
|
case "ibmi.libl.add": {
|
|
1513
1823
|
const name = getConnectionName(ctx, args);
|
|
@@ -1519,12 +1829,10 @@ export async function handleTool(ctx, name, args) {
|
|
|
1519
1829
|
if (!list.includes(lib))
|
|
1520
1830
|
list.push(lib);
|
|
1521
1831
|
const active = await ctx.ensureActive(name);
|
|
1522
|
-
enforceWritable(ctx, active, "qsys.write", args);
|
|
1832
|
+
await enforceWritable(ctx, active, "qsys.write", args);
|
|
1523
1833
|
await updateConnectionSettings(ctx, name, { libraryList: list });
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
}
|
|
1527
|
-
return result("OK");
|
|
1834
|
+
const applied = args.applyToJob ? await applyLibraryList(active, list, conn.settings?.currentLibrary) : undefined;
|
|
1835
|
+
return okApplied(applied);
|
|
1528
1836
|
}
|
|
1529
1837
|
case "ibmi.libl.remove": {
|
|
1530
1838
|
const name = getConnectionName(ctx, args);
|
|
@@ -1533,23 +1841,19 @@ export async function handleTool(ctx, name, args) {
|
|
|
1533
1841
|
throw new Error(`Connection ${name} not found`);
|
|
1534
1842
|
const list = (conn.settings?.libraryList || []).filter(l => l !== String(args.library).toUpperCase());
|
|
1535
1843
|
const active = await ctx.ensureActive(name);
|
|
1536
|
-
enforceWritable(ctx, active, "qsys.write", args);
|
|
1844
|
+
await enforceWritable(ctx, active, "qsys.write", args);
|
|
1537
1845
|
await updateConnectionSettings(ctx, name, { libraryList: list });
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
}
|
|
1541
|
-
return result("OK");
|
|
1846
|
+
const applied = args.applyToJob ? await applyLibraryList(active, list, conn.settings?.currentLibrary) : undefined;
|
|
1847
|
+
return okApplied(applied);
|
|
1542
1848
|
}
|
|
1543
1849
|
case "ibmi.libl.setCurrent": {
|
|
1544
1850
|
const name = getConnectionName(ctx, args);
|
|
1545
1851
|
const lib = String(args.library).toUpperCase();
|
|
1546
1852
|
const active = await ctx.ensureActive(name);
|
|
1547
|
-
enforceWritable(ctx, active, "qsys.write", args);
|
|
1853
|
+
await enforceWritable(ctx, active, "qsys.write", args);
|
|
1548
1854
|
await updateConnectionSettings(ctx, name, { currentLibrary: lib });
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
}
|
|
1552
|
-
return result("OK");
|
|
1855
|
+
const applied = args.applyToJob ? await applyLibraryList(active, undefined, lib) : undefined;
|
|
1856
|
+
return okApplied(applied);
|
|
1553
1857
|
}
|
|
1554
1858
|
case "ibmi.libl.validate": {
|
|
1555
1859
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
@@ -1583,17 +1887,15 @@ export async function handleTool(ctx, name, args) {
|
|
|
1583
1887
|
if (!profile)
|
|
1584
1888
|
throw new Error(`Profile ${args.name} not found`);
|
|
1585
1889
|
const active = await ctx.ensureActive(name);
|
|
1586
|
-
enforceWritable(ctx, active, "qsys.write", args);
|
|
1890
|
+
await enforceWritable(ctx, active, "qsys.write", args);
|
|
1587
1891
|
await ctx.store.setCurrentProfile(name, args.name);
|
|
1588
1892
|
await updateConnectionSettings(ctx, name, {
|
|
1589
1893
|
currentLibrary: profile.currentLibrary,
|
|
1590
1894
|
libraryList: profile.libraryList,
|
|
1591
1895
|
customVariables: profile.customVariables
|
|
1592
1896
|
});
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
}
|
|
1596
|
-
return result(`Profile ${args.name} activated`);
|
|
1897
|
+
const applied = args.applyToJob ? await applyLibraryList(active, profile.libraryList, profile.currentLibrary) : undefined;
|
|
1898
|
+
return okApplied(applied, `Profile ${args.name} activated`);
|
|
1597
1899
|
}
|
|
1598
1900
|
case "ibmi.resolve.path": {
|
|
1599
1901
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
@@ -1615,15 +1917,20 @@ export async function handleTool(ctx, name, args) {
|
|
|
1615
1917
|
}
|
|
1616
1918
|
case "ibmi.deploy.uploadDirectory": {
|
|
1617
1919
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1618
|
-
enforceWritable(ctx, conn, "deploy.sync", args);
|
|
1619
|
-
await
|
|
1920
|
+
await enforceWritable(ctx, conn, "deploy.sync", args);
|
|
1921
|
+
const localPath = await guardLocalPath(ctx, args.localPath, "read");
|
|
1922
|
+
await conn.client.putDirectory(localPath, args.remotePath, { recursive: true, concurrency: 5 });
|
|
1620
1923
|
return result("OK");
|
|
1621
1924
|
}
|
|
1622
1925
|
case "ibmi.deploy.uploadFiles": {
|
|
1623
1926
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1624
|
-
enforceWritable(ctx, conn, "deploy.sync", args);
|
|
1927
|
+
await enforceWritable(ctx, conn, "deploy.sync", args);
|
|
1625
1928
|
const remoteDir = String(args.remoteDirectory);
|
|
1929
|
+
const localFiles = [];
|
|
1626
1930
|
for (const file of args.localFiles || []) {
|
|
1931
|
+
localFiles.push(await guardLocalPath(ctx, file, "read"));
|
|
1932
|
+
}
|
|
1933
|
+
for (const file of localFiles) {
|
|
1627
1934
|
const base = String(file).split(/[\\/]/).pop();
|
|
1628
1935
|
const remotePath = `${remoteDir}/${base}`;
|
|
1629
1936
|
await conn.client.putFile(String(file), remotePath);
|
|
@@ -1632,12 +1939,12 @@ export async function handleTool(ctx, name, args) {
|
|
|
1632
1939
|
}
|
|
1633
1940
|
case "ibmi.deploy.setCcsid": {
|
|
1634
1941
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1635
|
-
enforceWritable(ctx, conn, "deploy.sync", args);
|
|
1942
|
+
await enforceWritable(ctx, conn, "deploy.sync", args);
|
|
1636
1943
|
const setccsid = conn.remoteFeatures.setccsid;
|
|
1637
1944
|
if (!setccsid)
|
|
1638
1945
|
throw new Error("setccsid not available on remote system");
|
|
1639
1946
|
const ccsid = args.ccsid || "1208";
|
|
1640
|
-
await conn.sendCommand({ command: `${setccsid} -R ${Tools.shellQuote(ccsid)} ${Tools.escapePath(args.remotePath)}` });
|
|
1947
|
+
requireCommandSuccess(await conn.sendCommand({ command: `${setccsid} -R ${Tools.shellQuote(ccsid)} ${Tools.escapePath(args.remotePath)}` }), "setccsid");
|
|
1641
1948
|
return result("OK");
|
|
1642
1949
|
}
|
|
1643
1950
|
case "ibmi.filters.list": {
|
|
@@ -1710,46 +2017,90 @@ export async function handleTool(ctx, name, args) {
|
|
|
1710
2017
|
}
|
|
1711
2018
|
case "ibmi.debug.startService": {
|
|
1712
2019
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1713
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2020
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
1714
2021
|
const submitOptions = `JOBQ(QSYS/QUSRNOMAX) JOBD(QSYS/QSYSJOBD) OUTQ(QUSRSYS/QDBGSRV) USER(QDBGSRV)`;
|
|
1715
2022
|
const cmd = `QSYS/SBMJOB JOB(QDBGSRV) SYSLIBL(*SYSVAL) CURLIB(*USRPRF) INLLIBL(*JOBD) ${submitOptions} CMD(QSH CMD('/QIBM/ProdData/IBMiDebugService/bin/startDebugService.sh'))`;
|
|
1716
2023
|
const res = await conn.sendQsh({ command: `system \"${cmd}\"` });
|
|
1717
|
-
return
|
|
2024
|
+
return commandOutcome({ ok: res.code === 0, ...res });
|
|
1718
2025
|
}
|
|
1719
2026
|
case "ibmi.debug.stopService": {
|
|
1720
2027
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1721
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2028
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
1722
2029
|
const res = await conn.sendCommand({ command: `/QIBM/ProdData/IBMiDebugService/bin/stopDebugService.sh` });
|
|
1723
|
-
return
|
|
2030
|
+
return commandOutcome({ ok: res.code === 0, ...res });
|
|
1724
2031
|
}
|
|
1725
2032
|
case "ibmi.deploy.compare": {
|
|
1726
2033
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1727
|
-
const localPath =
|
|
2034
|
+
const localPath = await guardLocalPath(ctx, args.localPath, "read");
|
|
1728
2035
|
const remotePath = String(args.remotePath);
|
|
2036
|
+
const method = normalizeDeployMethod(args.method);
|
|
2037
|
+
if (method !== "all" && method !== "changed")
|
|
2038
|
+
throw new Error(`deploy.compare supports method "all" or "changed", not "${method}"`);
|
|
1729
2039
|
const comparison = await compareDeployTrees(conn, localPath, remotePath);
|
|
1730
|
-
return json(comparison);
|
|
2040
|
+
return json(method === "changed" ? await refineComparisonByHash(conn, comparison) : comparison);
|
|
1731
2041
|
}
|
|
1732
2042
|
case "ibmi.deploy.sync": {
|
|
1733
2043
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1734
|
-
enforceWritable(ctx, conn, "deploy.sync", args);
|
|
1735
|
-
const localPath =
|
|
2044
|
+
await enforceWritable(ctx, conn, "deploy.sync", args);
|
|
2045
|
+
const localPath = await guardLocalPath(ctx, args.localPath, "read");
|
|
1736
2046
|
const remotePath = String(args.remotePath);
|
|
2047
|
+
const method = normalizeDeployMethod(args.method);
|
|
1737
2048
|
const overwrite = Boolean(args.overwrite);
|
|
1738
2049
|
const dryRun = Boolean(args.dryRun);
|
|
1739
2050
|
const deleteExtraRemote = Boolean(args.deleteExtraRemote);
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
2051
|
+
if (deleteExtraRemote && (method === "gitChanged" || method === "selection")) {
|
|
2052
|
+
throw new Error(`deleteExtraRemote is only supported with method "all" or "changed"`);
|
|
2053
|
+
}
|
|
2054
|
+
const errors = [];
|
|
2055
|
+
let comparison;
|
|
2056
|
+
let uploadSet;
|
|
2057
|
+
let skipChanged = [];
|
|
2058
|
+
let unresolved = [];
|
|
2059
|
+
let unchanged = [];
|
|
2060
|
+
let gitDeleted = [];
|
|
2061
|
+
if (method === "all" || method === "changed") {
|
|
2062
|
+
comparison = await compareDeployTrees(conn, localPath, remotePath);
|
|
2063
|
+
if (method === "changed")
|
|
2064
|
+
comparison = await refineComparisonByHash(conn, comparison);
|
|
2065
|
+
uploadSet = new Set(comparison.onlyLocal);
|
|
2066
|
+
// "changed" compared content, so every differing file is uploaded.
|
|
2067
|
+
const includeChanged = overwrite || method === "changed";
|
|
2068
|
+
if (includeChanged) {
|
|
2069
|
+
comparison.changed.forEach(entry => uploadSet.add(entry.path));
|
|
2070
|
+
comparison.unresolved.forEach(entry => uploadSet.add(entry.path));
|
|
2071
|
+
}
|
|
2072
|
+
skipChanged = includeChanged ? [] : comparison.changed.map(entry => entry.path).sort((a, b) => a.localeCompare(b));
|
|
2073
|
+
unresolved = includeChanged ? [] : comparison.unresolved.map(entry => entry.path).sort((a, b) => a.localeCompare(b));
|
|
2074
|
+
unchanged = [...comparison.identical];
|
|
2075
|
+
}
|
|
2076
|
+
else if (method === "gitChanged") {
|
|
2077
|
+
const git = await gitChangedFiles(localPath);
|
|
2078
|
+
uploadSet = new Set(git.files);
|
|
2079
|
+
gitDeleted = git.deleted;
|
|
2080
|
+
}
|
|
2081
|
+
else {
|
|
2082
|
+
uploadSet = new Set();
|
|
2083
|
+
for (const rel of normalizeDeploySelection(args.files)) {
|
|
2084
|
+
try {
|
|
2085
|
+
const resolved = await guardLocalPath(ctx, path.join(localPath, rel), "read");
|
|
2086
|
+
const relative = path.relative(localPath, resolved);
|
|
2087
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
2088
|
+
throw new Error("file is outside localPath");
|
|
2089
|
+
}
|
|
2090
|
+
const stat = await fs.stat(resolved).catch(() => undefined);
|
|
2091
|
+
if (!stat?.isFile())
|
|
2092
|
+
throw new Error("local file not found");
|
|
2093
|
+
uploadSet.add(rel);
|
|
2094
|
+
}
|
|
2095
|
+
catch (err) {
|
|
2096
|
+
errors.push({ operation: "upload", path: rel, message: err?.message || String(err) });
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
1745
2099
|
}
|
|
1746
2100
|
const uploadPlan = Array.from(uploadSet).sort((a, b) => a.localeCompare(b));
|
|
1747
|
-
const
|
|
1748
|
-
const unresolved = overwrite ? [] : comparison.unresolved.map(entry => entry.path).sort((a, b) => a.localeCompare(b));
|
|
1749
|
-
const deletePlan = deleteExtraRemote ? [...comparison.onlyRemote] : [];
|
|
2101
|
+
const deletePlan = deleteExtraRemote && comparison ? [...comparison.onlyRemote] : [];
|
|
1750
2102
|
const uploaded = [];
|
|
1751
2103
|
const deleted = [];
|
|
1752
|
-
const errors = [];
|
|
1753
2104
|
if (!dryRun) {
|
|
1754
2105
|
const createdDirs = new Set();
|
|
1755
2106
|
for (const rel of uploadPlan) {
|
|
@@ -1758,7 +2109,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
1758
2109
|
const remoteDir = remoteFile.split("/").slice(0, -1).join("/");
|
|
1759
2110
|
try {
|
|
1760
2111
|
if (!createdDirs.has(remoteDir)) {
|
|
1761
|
-
await conn.sendCommand({ command: `mkdir -p ${Tools.escapePath(remoteDir)}` });
|
|
2112
|
+
requireCommandSuccess(await conn.sendCommand({ command: `mkdir -p ${Tools.escapePath(remoteDir)}` }), "mkdir");
|
|
1762
2113
|
createdDirs.add(remoteDir);
|
|
1763
2114
|
}
|
|
1764
2115
|
await conn.client.putFile(localFile, remoteFile);
|
|
@@ -1771,7 +2122,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
1771
2122
|
for (const rel of deletePlan) {
|
|
1772
2123
|
const remoteFile = joinRemotePath(remotePath, rel);
|
|
1773
2124
|
try {
|
|
1774
|
-
await conn.sendCommand({ command: `rm -f ${Tools.escapePath(remoteFile)}` });
|
|
2125
|
+
requireCommandSuccess(await conn.sendCommand({ command: `rm -f ${Tools.escapePath(remoteFile)}` }), "rm");
|
|
1775
2126
|
deleted.push(rel);
|
|
1776
2127
|
}
|
|
1777
2128
|
catch (err) {
|
|
@@ -1779,22 +2130,35 @@ export async function handleTool(ctx, name, args) {
|
|
|
1779
2130
|
}
|
|
1780
2131
|
}
|
|
1781
2132
|
}
|
|
1782
|
-
|
|
2133
|
+
const syncResult = {
|
|
1783
2134
|
ok: errors.length === 0,
|
|
1784
|
-
mode: { overwrite, deleteExtraRemote, dryRun },
|
|
1785
|
-
comparison,
|
|
2135
|
+
mode: { method, overwrite, deleteExtraRemote, dryRun },
|
|
2136
|
+
comparison: comparison ?? null,
|
|
1786
2137
|
planned: {
|
|
1787
2138
|
upload: uploadPlan,
|
|
1788
2139
|
delete: deletePlan,
|
|
1789
2140
|
skippedChanged: skipChanged,
|
|
1790
|
-
unresolved
|
|
2141
|
+
unresolved,
|
|
2142
|
+
...(method === "gitChanged" ? { gitDeleted } : {})
|
|
1791
2143
|
},
|
|
1792
2144
|
applied: {
|
|
1793
2145
|
uploaded: dryRun ? [] : uploaded,
|
|
1794
2146
|
deleted: dryRun ? [] : deleted
|
|
1795
2147
|
},
|
|
2148
|
+
summary: {
|
|
2149
|
+
planned: uploadPlan.length,
|
|
2150
|
+
uploaded: dryRun ? 0 : uploaded.length,
|
|
2151
|
+
unchanged: unchanged.length,
|
|
2152
|
+
skipped: skipChanged.length + unresolved.length + gitDeleted.length,
|
|
2153
|
+
deleted: dryRun ? 0 : deleted.length,
|
|
2154
|
+
failed: errors.length
|
|
2155
|
+
},
|
|
1796
2156
|
errors
|
|
1797
|
-
}
|
|
2157
|
+
};
|
|
2158
|
+
if (errors.length > 0) {
|
|
2159
|
+
return jsonError(`deploy.sync finished with ${errors.length} failed operation(s)`, syncResult);
|
|
2160
|
+
}
|
|
2161
|
+
return json(syncResult);
|
|
1798
2162
|
}
|
|
1799
2163
|
case "ibmi.sql.query": {
|
|
1800
2164
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
@@ -1815,10 +2179,14 @@ export async function handleTool(ctx, name, args) {
|
|
|
1815
2179
|
if (!args?.sql)
|
|
1816
2180
|
throw new Error("sql is required when cursor is not provided");
|
|
1817
2181
|
const sql = String(args.sql);
|
|
1818
|
-
|
|
1819
|
-
|
|
2182
|
+
const readOnlyCheck = checkReadOnlySql(sql);
|
|
2183
|
+
if (!readOnlyCheck.readOnly)
|
|
2184
|
+
throw new Error(`ibmi.sql.query only supports read-only SQL: ${readOnlyCheck.reason}`);
|
|
1820
2185
|
const startedAt = Date.now();
|
|
1821
|
-
|
|
2186
|
+
// Ask for one row more than maxRows so truncation can be reported without
|
|
2187
|
+
// pulling the whole result set over the wire.
|
|
2188
|
+
const capped = applySqlRowCap(sql, maxRows + 1);
|
|
2189
|
+
const rows = await runSqlWithTimeout(conn, capped.sql, timeoutMs, `SQL query timed out after ${timeoutMs}ms`);
|
|
1822
2190
|
const limited = rows.slice(0, maxRows);
|
|
1823
2191
|
const firstPage = limited.slice(0, pageSize);
|
|
1824
2192
|
const cursorInfo = ctx.createSqlCursor(limited, pageSize);
|
|
@@ -1828,6 +2196,9 @@ export async function handleTool(ctx, name, args) {
|
|
|
1828
2196
|
cursor: cursorInfo.cursor,
|
|
1829
2197
|
totalRows: limited.length,
|
|
1830
2198
|
truncated: rows.length > limited.length,
|
|
2199
|
+
...(rows.length > limited.length
|
|
2200
|
+
? { truncationNote: `Result has more than ${maxRows} rows; only the first ${maxRows} are returned. Raise maxRows (up to 20000) or narrow the query.` }
|
|
2201
|
+
: {}),
|
|
1831
2202
|
...(includeMetadata
|
|
1832
2203
|
? {
|
|
1833
2204
|
metadata: {
|
|
@@ -1835,6 +2206,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
1835
2206
|
columns: getRowColumns(firstPage),
|
|
1836
2207
|
pageSize,
|
|
1837
2208
|
maxRows,
|
|
2209
|
+
rowCapAppliedInSql: capped.applied,
|
|
1838
2210
|
timeoutMs,
|
|
1839
2211
|
executionMs
|
|
1840
2212
|
}
|
|
@@ -1844,13 +2216,13 @@ export async function handleTool(ctx, name, args) {
|
|
|
1844
2216
|
}
|
|
1845
2217
|
case "ibmi.sql.execute": {
|
|
1846
2218
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1847
|
-
|
|
2219
|
+
await enforceWritable(ctx, conn, "sql.write", args);
|
|
1848
2220
|
const sql = String(args.sql);
|
|
1849
2221
|
const timeoutMs = clampNumber(args?.timeoutMs, 45000, 1000, 300000);
|
|
1850
2222
|
const statementType = getSqlStatementType(sql);
|
|
1851
2223
|
const startedAt = Date.now();
|
|
1852
2224
|
try {
|
|
1853
|
-
const rows = await
|
|
2225
|
+
const rows = await runSqlWithTimeout(conn, sql, timeoutMs, `SQL execute timed out after ${timeoutMs}ms`);
|
|
1854
2226
|
const executionMs = Date.now() - startedAt;
|
|
1855
2227
|
const affectedRows = inferAffectedRows(rows);
|
|
1856
2228
|
log("info", "audit.sql.execute", {
|
|
@@ -1883,7 +2255,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
1883
2255
|
}
|
|
1884
2256
|
case "ibmi.cl.run": {
|
|
1885
2257
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1886
|
-
|
|
2258
|
+
await enforceWritable(ctx, conn, "cl.run", args);
|
|
1887
2259
|
const command = String(args.command || "");
|
|
1888
2260
|
if (!command.trim())
|
|
1889
2261
|
throw new Error("command is required");
|
|
@@ -1905,13 +2277,24 @@ export async function handleTool(ctx, name, args) {
|
|
|
1905
2277
|
case "ibmi.joblog.get": {
|
|
1906
2278
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1907
2279
|
const limit = clampNumber(args?.limit, 200, 1, 5000);
|
|
1908
|
-
|
|
2280
|
+
// MCH3401 "Cannot resolve to object PPTT" is logged by the database
|
|
2281
|
+
// machinery on every statement on some systems; it would crowd out the
|
|
2282
|
+
// messages from the user's own SQL and CL.
|
|
2283
|
+
const rows = await conn.runSQL(`select message_id, message_text, severity, message_timestamp from table(qsys2.joblog_info('*')) x ` +
|
|
2284
|
+
`where not (coalesce(message_id, '') = 'MCH3401' and coalesce(message_text, '') like '%PPTT%') ` +
|
|
2285
|
+
`order by message_timestamp desc fetch first ${limit} rows only`);
|
|
1909
2286
|
return json(rows.map(normalizeJoblogRow));
|
|
1910
2287
|
}
|
|
1911
2288
|
case "ibmi.spool.list": {
|
|
1912
2289
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1913
2290
|
const limit = clampNumber(args?.limit, 200, 1, 5000);
|
|
1914
|
-
const rows = await querySpoolEntries(conn,
|
|
2291
|
+
const rows = await querySpoolEntries(conn, {
|
|
2292
|
+
limit,
|
|
2293
|
+
userName: args?.userName ? requireIbmObjectName(args.userName, "userName") : undefined,
|
|
2294
|
+
outputQueue: args?.outputQueue ? requireIbmObjectName(args.outputQueue, "outputQueue") : undefined,
|
|
2295
|
+
outputQueueLibrary: args?.outputQueueLibrary ? requireIbmObjectName(args.outputQueueLibrary, "outputQueueLibrary") : undefined,
|
|
2296
|
+
jobName: args?.jobName ? requireSpoolJobFilter(args.jobName) : undefined
|
|
2297
|
+
});
|
|
1915
2298
|
return json(rows.map(normalizeSpoolEntry));
|
|
1916
2299
|
}
|
|
1917
2300
|
case "ibmi.spool.read": {
|
|
@@ -1920,39 +2303,41 @@ export async function handleTool(ctx, name, args) {
|
|
|
1920
2303
|
const jobName = Tools.sqlString(String(args.jobName));
|
|
1921
2304
|
const fileName = Tools.sqlString(String(args.spooledFileName));
|
|
1922
2305
|
const fileNbr = clampNumber(args.spooledFileNumber, 1, 1, 999999999);
|
|
1923
|
-
|
|
2306
|
+
// DISPLAY_SPOOLED_FILE_DATA returns ORDINAL_POSITION and SPOOLED_DATA.
|
|
2307
|
+
const rows = await conn.runSQL(`select ordinal_position, spooled_data from table(qsys2.display_spooled_file_data(${jobName}, ${fileName}, ${fileNbr})) ` +
|
|
2308
|
+
`order by ordinal_position fetch first ${limit} rows only`);
|
|
1924
2309
|
return json(rows.map(normalizeSpoolLine));
|
|
1925
2310
|
}
|
|
1926
2311
|
case "ibmi.spool.hold": {
|
|
1927
2312
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1928
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
1929
|
-
const command = buildSpoolCommand("
|
|
2313
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
2314
|
+
const command = buildSpoolCommand("HLDSPLF", args);
|
|
1930
2315
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
1931
|
-
return
|
|
2316
|
+
return commandOutcome({ ok: cmdResult.code === 0, command, ...cmdResult });
|
|
1932
2317
|
}
|
|
1933
2318
|
case "ibmi.spool.release": {
|
|
1934
2319
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1935
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2320
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
1936
2321
|
const command = buildSpoolCommand("RLSSPLF", args);
|
|
1937
2322
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
1938
|
-
return
|
|
2323
|
+
return commandOutcome({ ok: cmdResult.code === 0, command, ...cmdResult });
|
|
1939
2324
|
}
|
|
1940
2325
|
case "ibmi.spool.delete": {
|
|
1941
2326
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1942
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2327
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
1943
2328
|
const command = buildSpoolCommand("DLTSPLF", args);
|
|
1944
2329
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
1945
|
-
return
|
|
2330
|
+
return commandOutcome({ ok: cmdResult.code === 0, command, ...cmdResult });
|
|
1946
2331
|
}
|
|
1947
2332
|
case "ibmi.spool.move": {
|
|
1948
2333
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1949
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2334
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
1950
2335
|
const baseCommand = buildSpoolCommand("CHGSPLFA", args);
|
|
1951
2336
|
const outQueueLibrary = requireIbmObjectName(args.outQueueLibrary, "outQueueLibrary");
|
|
1952
2337
|
const outQueue = requireIbmObjectName(args.outQueue, "outQueue");
|
|
1953
2338
|
const command = `${baseCommand} OUTQ(${outQueueLibrary}/${outQueue})`;
|
|
1954
2339
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
1955
|
-
return
|
|
2340
|
+
return commandOutcome({ ok: cmdResult.code === 0, command, ...cmdResult });
|
|
1956
2341
|
}
|
|
1957
2342
|
case "ibmi.jobs.list": {
|
|
1958
2343
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
@@ -1967,24 +2352,24 @@ export async function handleTool(ctx, name, args) {
|
|
|
1967
2352
|
}
|
|
1968
2353
|
case "ibmi.jobs.hold": {
|
|
1969
2354
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1970
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2355
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
1971
2356
|
const jobName = requireJobName(args.jobName);
|
|
1972
2357
|
const hold = Boolean(args?.holdOnJobQueue) ? "*JOBQ" : "*NO";
|
|
1973
2358
|
const command = `HLDJOB JOB(${jobName}) HOLD(${hold})`;
|
|
1974
2359
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
1975
|
-
return
|
|
2360
|
+
return commandOutcome({ ok: cmdResult.code === 0, command, ...cmdResult });
|
|
1976
2361
|
}
|
|
1977
2362
|
case "ibmi.jobs.release": {
|
|
1978
2363
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1979
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2364
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
1980
2365
|
const jobName = requireJobName(args.jobName);
|
|
1981
2366
|
const command = `RLSJOB JOB(${jobName})`;
|
|
1982
2367
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
1983
|
-
return
|
|
2368
|
+
return commandOutcome({ ok: cmdResult.code === 0, command, ...cmdResult });
|
|
1984
2369
|
}
|
|
1985
2370
|
case "ibmi.jobs.end": {
|
|
1986
2371
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1987
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2372
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
1988
2373
|
const jobName = requireJobName(args.jobName);
|
|
1989
2374
|
const option = String(args?.option || "*CNTRLD").toUpperCase();
|
|
1990
2375
|
if (option !== "*CNTRLD" && option !== "*IMMED") {
|
|
@@ -1993,7 +2378,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
1993
2378
|
const delaySeconds = clampNumber(args?.delaySeconds, 30, 0, 99999);
|
|
1994
2379
|
const command = `ENDJOB JOB(${jobName}) OPTION(${option}) DELAY(${delaySeconds})`;
|
|
1995
2380
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
1996
|
-
return
|
|
2381
|
+
return commandOutcome({ ok: cmdResult.code === 0, command, ...cmdResult });
|
|
1997
2382
|
}
|
|
1998
2383
|
case "ibmi.subsystems.list": {
|
|
1999
2384
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
@@ -2014,15 +2399,15 @@ export async function handleTool(ctx, name, args) {
|
|
|
2014
2399
|
}
|
|
2015
2400
|
case "ibmi.subsystems.start": {
|
|
2016
2401
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
2017
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2402
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
2018
2403
|
const subsystemDescription = requireQualifiedObject(args.subsystemDescription, "subsystemDescription");
|
|
2019
2404
|
const command = `STRSBS SBSD(${subsystemDescription.library}/${subsystemDescription.object})`;
|
|
2020
2405
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
2021
|
-
return
|
|
2406
|
+
return commandOutcome({ ok: cmdResult.code === 0, command, ...cmdResult });
|
|
2022
2407
|
}
|
|
2023
2408
|
case "ibmi.subsystems.end": {
|
|
2024
2409
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
2025
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2410
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
2026
2411
|
const subsystem = requireIbmObjectName(args.subsystem, "subsystem");
|
|
2027
2412
|
const option = String(args?.option || "*CNTRLD").toUpperCase();
|
|
2028
2413
|
if (option !== "*CNTRLD" && option !== "*IMMED") {
|
|
@@ -2031,7 +2416,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
2031
2416
|
const delaySeconds = clampNumber(args?.delaySeconds, 30, 0, 99999);
|
|
2032
2417
|
const command = `ENDSBS SBS(${subsystem}) OPTION(${option}) DELAY(${delaySeconds})`;
|
|
2033
2418
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
2034
|
-
return
|
|
2419
|
+
return commandOutcome({ ok: cmdResult.code === 0, command, ...cmdResult });
|
|
2035
2420
|
}
|
|
2036
2421
|
case "ibmi.msgq.read": {
|
|
2037
2422
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
@@ -2043,7 +2428,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
2043
2428
|
}
|
|
2044
2429
|
case "ibmi.msgq.send": {
|
|
2045
2430
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
2046
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2431
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
2047
2432
|
const library = requireIbmObjectName(args?.library || "QSYS", "library");
|
|
2048
2433
|
const messageQueue = requireIbmObjectName(args?.messageQueue || "QSYSOPR", "messageQueue");
|
|
2049
2434
|
const messageType = String(args?.messageType || "*INFO").toUpperCase();
|
|
@@ -2053,16 +2438,19 @@ export async function handleTool(ctx, name, args) {
|
|
|
2053
2438
|
const message = String(args.message || "");
|
|
2054
2439
|
if (!message.trim())
|
|
2055
2440
|
throw new Error("message is required");
|
|
2056
|
-
const
|
|
2057
|
-
const command = mappedMessageType === "*INQ"
|
|
2058
|
-
? `SNDMSG MSG(${quoteClString(message)}) TOMSGQ(${library}/${messageQueue}) MSGTYPE(*INQ) RPYMSGQ(${library}/${messageQueue})`
|
|
2059
|
-
: `SNDMSG MSG(${quoteClString(message)}) TOMSGQ(${library}/${messageQueue}) MSGTYPE(*INFO)`;
|
|
2441
|
+
const command = buildSendMessageCommand(message, library, messageQueue, messageType);
|
|
2060
2442
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
2061
|
-
return
|
|
2443
|
+
return commandOutcome({
|
|
2444
|
+
ok: cmdResult.code === 0,
|
|
2445
|
+
command,
|
|
2446
|
+
requestedMessageType: messageType,
|
|
2447
|
+
mappedMessageType: messageType,
|
|
2448
|
+
...cmdResult
|
|
2449
|
+
});
|
|
2062
2450
|
}
|
|
2063
2451
|
case "ibmi.msgq.reply": {
|
|
2064
2452
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
2065
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2453
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
2066
2454
|
const library = requireIbmObjectName(args?.library || "QSYS", "library");
|
|
2067
2455
|
const messageQueue = requireIbmObjectName(args?.messageQueue || "QSYSOPR", "messageQueue");
|
|
2068
2456
|
const messageKey = requireMessageKey(args.messageKey);
|
|
@@ -2071,16 +2459,16 @@ export async function handleTool(ctx, name, args) {
|
|
|
2071
2459
|
throw new Error("reply is required");
|
|
2072
2460
|
const command = `SNDRPY MSGKEY(${messageKey}) MSGQ(${library}/${messageQueue}) RPY(${quoteClString(reply)})`;
|
|
2073
2461
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
2074
|
-
return
|
|
2462
|
+
return commandOutcome({ ok: cmdResult.code === 0, command, ...cmdResult });
|
|
2075
2463
|
}
|
|
2076
2464
|
case "ibmi.locks.list": {
|
|
2077
2465
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
2078
2466
|
const limit = clampNumber(args?.limit, 200, 1, 5000);
|
|
2079
2467
|
const rows = await queryLockEntries(conn, {
|
|
2080
|
-
objectLibrary: args?.objectLibrary,
|
|
2081
|
-
objectName: args?.objectName,
|
|
2082
|
-
objectType: args?.objectType,
|
|
2083
|
-
member: args?.member,
|
|
2468
|
+
objectLibrary: args?.objectLibrary ? requireIbmObjectName(args.objectLibrary, "objectLibrary") : undefined,
|
|
2469
|
+
objectName: args?.objectName ? requireIbmObjectName(args.objectName, "objectName") : undefined,
|
|
2470
|
+
objectType: args?.objectType ? requireObjectTypeValue(args.objectType, "objectType") : undefined,
|
|
2471
|
+
member: args?.member ? requireIbmObjectName(args.member, "member") : undefined,
|
|
2084
2472
|
limit
|
|
2085
2473
|
});
|
|
2086
2474
|
return json(rows);
|
|
@@ -2096,14 +2484,16 @@ export async function handleTool(ctx, name, args) {
|
|
|
2096
2484
|
}
|
|
2097
2485
|
case "ibmi.dataqueue.send": {
|
|
2098
2486
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
2099
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2487
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
2100
2488
|
const library = requireIbmObjectName(args.library, "library");
|
|
2101
2489
|
const queue = requireIbmObjectName(args.queue, "queue");
|
|
2102
2490
|
const message = String(args.message || "");
|
|
2103
2491
|
if (!message)
|
|
2104
2492
|
throw new Error("message is required");
|
|
2493
|
+
let primaryError;
|
|
2105
2494
|
try {
|
|
2106
|
-
await conn.runSQL(`call qsys2.send_data_queue(${Tools.sqlString(message)},
|
|
2495
|
+
await conn.runSQL(`call qsys2.send_data_queue(message_data => ${Tools.sqlString(message)}, ` +
|
|
2496
|
+
`data_queue => ${Tools.sqlString(queue)}, data_queue_library => ${Tools.sqlString(library)})`);
|
|
2107
2497
|
return json({
|
|
2108
2498
|
ok: true,
|
|
2109
2499
|
method: "qsys2.send_data_queue",
|
|
@@ -2112,18 +2502,31 @@ export async function handleTool(ctx, name, args) {
|
|
|
2112
2502
|
messageLength: message.length
|
|
2113
2503
|
});
|
|
2114
2504
|
}
|
|
2115
|
-
catch {
|
|
2116
|
-
|
|
2117
|
-
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
2118
|
-
return json({ ok: cmdResult.code === 0, method: "cl.snddtaq", command, ...cmdResult });
|
|
2505
|
+
catch (err) {
|
|
2506
|
+
primaryError = err?.message || String(err);
|
|
2119
2507
|
}
|
|
2508
|
+
// Older releases without QSYS2.SEND_DATA_QUEUE: call the QSNDDTAQ API.
|
|
2509
|
+
// There is no SNDDTAQ CL command.
|
|
2510
|
+
const command = buildQsnddtaqCall(library, queue, message);
|
|
2511
|
+
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
2512
|
+
return commandOutcome({
|
|
2513
|
+
ok: cmdResult.code === 0,
|
|
2514
|
+
method: "api.qsnddtaq",
|
|
2515
|
+
command,
|
|
2516
|
+
primaryError,
|
|
2517
|
+
...cmdResult
|
|
2518
|
+
});
|
|
2120
2519
|
}
|
|
2121
2520
|
case "ibmi.dataqueue.receive": {
|
|
2122
2521
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
2123
2522
|
const library = requireIbmObjectName(args.library, "library");
|
|
2124
2523
|
const queue = requireIbmObjectName(args.queue, "queue");
|
|
2125
|
-
const waitSeconds = clampNumber(args?.waitSeconds, 0, 0,
|
|
2126
|
-
|
|
2524
|
+
const waitSeconds = clampNumber(args?.waitSeconds, 0, 0, DATA_QUEUE_MAX_WAIT_SECONDS);
|
|
2525
|
+
// Peek by default. Removing an entry changes the queue, so it goes
|
|
2526
|
+
// through the same write gate as the other write tools.
|
|
2527
|
+
const remove = args?.remove === true;
|
|
2528
|
+
if (remove)
|
|
2529
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
2127
2530
|
const rows = await queryDataQueueEntries(conn, { library, queue, waitSeconds, remove });
|
|
2128
2531
|
return json(rows);
|
|
2129
2532
|
}
|
|
@@ -2136,15 +2539,20 @@ export async function handleTool(ctx, name, args) {
|
|
|
2136
2539
|
}
|
|
2137
2540
|
case "ibmi.dataarea.write": {
|
|
2138
2541
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
2139
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2542
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
2140
2543
|
const library = requireIbmObjectName(args.library, "library");
|
|
2141
2544
|
const dataArea = requireIbmObjectName(args.dataArea, "dataArea");
|
|
2142
|
-
const value = String(args.value
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2545
|
+
const value = String(args.value ?? "");
|
|
2546
|
+
let info;
|
|
2547
|
+
try {
|
|
2548
|
+
info = (await queryDataAreaInfo(conn, { library, dataArea }))[0];
|
|
2549
|
+
}
|
|
2550
|
+
catch {
|
|
2551
|
+
info = undefined;
|
|
2552
|
+
}
|
|
2553
|
+
const command = buildChangeDataAreaCommand(library, dataArea, value, info, args);
|
|
2146
2554
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
2147
|
-
return
|
|
2555
|
+
return commandOutcome({ ok: cmdResult.code === 0, command, dataAreaType: info?.type || null, ...cmdResult });
|
|
2148
2556
|
}
|
|
2149
2557
|
case "ibmi.audit.list": {
|
|
2150
2558
|
const data = await listAuditRecords({
|
|
@@ -2163,7 +2571,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
2163
2571
|
}
|
|
2164
2572
|
case "ibmi.audit.export": {
|
|
2165
2573
|
const exported = await exportAuditRecords({
|
|
2166
|
-
outputPath:
|
|
2574
|
+
outputPath: await guardLocalPath(ctx, args.outputPath, "write"),
|
|
2167
2575
|
format: args?.format,
|
|
2168
2576
|
filter: {
|
|
2169
2577
|
limit: args?.limit,
|
|
@@ -2177,8 +2585,8 @@ export async function handleTool(ctx, name, args) {
|
|
|
2177
2585
|
return json(exported);
|
|
2178
2586
|
}
|
|
2179
2587
|
case "ibmi.audit.purge": {
|
|
2180
|
-
if (!Boolean(args?.dryRun)
|
|
2181
|
-
|
|
2588
|
+
if (!Boolean(args?.dryRun)) {
|
|
2589
|
+
await requireDangerousApproval(ctx, "audit.purge", args, "ibmi.audit.purge requires approve=true unless dryRun=true.");
|
|
2182
2590
|
}
|
|
2183
2591
|
const purged = await purgeAuditRecords({
|
|
2184
2592
|
before: String(args.before),
|
|
@@ -2197,6 +2605,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
2197
2605
|
const journal = requireIbmObjectName(args.journal, "journal");
|
|
2198
2606
|
const objectLibrary = args?.objectLibrary ? requireIbmObjectName(args.objectLibrary, "objectLibrary") : undefined;
|
|
2199
2607
|
const objectName = args?.objectName ? requireIbmObjectName(args.objectName, "objectName") : undefined;
|
|
2608
|
+
const objectType = args?.objectType ? requireObjectTypeValue(args.objectType, "objectType") : undefined;
|
|
2200
2609
|
const journalCode = args?.journalCode ? requireJournalCode(args.journalCode) : undefined;
|
|
2201
2610
|
const entryType = args?.entryType ? requireJournalEntryType(args.entryType) : undefined;
|
|
2202
2611
|
const userName = args?.userName ? requireIbmObjectName(args.userName, "userName") : undefined;
|
|
@@ -2210,6 +2619,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
2210
2619
|
journal,
|
|
2211
2620
|
objectLibrary,
|
|
2212
2621
|
objectName,
|
|
2622
|
+
objectType,
|
|
2213
2623
|
journalCode,
|
|
2214
2624
|
entryType,
|
|
2215
2625
|
userName,
|
|
@@ -2250,13 +2660,13 @@ export async function handleTool(ctx, name, args) {
|
|
|
2250
2660
|
}
|
|
2251
2661
|
case "ibmi.journal.receiver.create": {
|
|
2252
2662
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
2253
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2663
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
2254
2664
|
const lib = requireIbmObjectName(args.library, "library");
|
|
2255
2665
|
const rcv = requireIbmObjectName(args.receiver, "receiver");
|
|
2256
2666
|
const threshold = clampNumber(args?.threshold, 1000000, 1, 1000000000);
|
|
2257
2667
|
const command = `CRTJRNRCV JRNRCV(${lib}/${rcv}) THRESHOLD(${threshold})`;
|
|
2258
2668
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
2259
|
-
return
|
|
2669
|
+
return commandOutcome({
|
|
2260
2670
|
ok: cmdResult.code === 0,
|
|
2261
2671
|
command,
|
|
2262
2672
|
...cmdResult
|
|
@@ -2264,14 +2674,14 @@ export async function handleTool(ctx, name, args) {
|
|
|
2264
2674
|
}
|
|
2265
2675
|
case "ibmi.journal.create": {
|
|
2266
2676
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
2267
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2677
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
2268
2678
|
const lib = requireIbmObjectName(args.library, "library");
|
|
2269
2679
|
const jrn = requireIbmObjectName(args.journal, "journal");
|
|
2270
2680
|
const rcvLib = requireIbmObjectName(args.receiverLibrary, "receiverLibrary");
|
|
2271
2681
|
const rcv = requireIbmObjectName(args.receiver, "receiver");
|
|
2272
2682
|
const command = `CRTJRN JRN(${lib}/${jrn}) JRNRCV(${rcvLib}/${rcv})`;
|
|
2273
2683
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
2274
|
-
return
|
|
2684
|
+
return commandOutcome({
|
|
2275
2685
|
ok: cmdResult.code === 0,
|
|
2276
2686
|
command,
|
|
2277
2687
|
...cmdResult
|
|
@@ -2279,14 +2689,14 @@ export async function handleTool(ctx, name, args) {
|
|
|
2279
2689
|
}
|
|
2280
2690
|
case "ibmi.journal.receiver.change": {
|
|
2281
2691
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
2282
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2692
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
2283
2693
|
const jrnLib = requireIbmObjectName(args.journalLibrary, "journalLibrary");
|
|
2284
2694
|
const jrn = requireIbmObjectName(args.journal, "journal");
|
|
2285
2695
|
const rcvLib = requireIbmObjectName(args.receiverLibrary, "receiverLibrary");
|
|
2286
2696
|
const rcv = requireIbmObjectName(args.receiver, "receiver");
|
|
2287
2697
|
const command = `CHGJRN JRN(${jrnLib}/${jrn}) JRNRCV(${rcvLib}/${rcv})`;
|
|
2288
2698
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
2289
|
-
return
|
|
2699
|
+
return commandOutcome({
|
|
2290
2700
|
ok: cmdResult.code === 0,
|
|
2291
2701
|
command,
|
|
2292
2702
|
...cmdResult
|
|
@@ -2294,7 +2704,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
2294
2704
|
}
|
|
2295
2705
|
case "ibmi.journal.startPf": {
|
|
2296
2706
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
2297
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2707
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
2298
2708
|
const lib = requireIbmObjectName(args.library, "library");
|
|
2299
2709
|
const file = requireIbmObjectName(args.file, "file");
|
|
2300
2710
|
const jrnLib = requireIbmObjectName(args.journalLibrary, "journalLibrary");
|
|
@@ -2305,7 +2715,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
2305
2715
|
}
|
|
2306
2716
|
const command = `STRJRNPF FILE(${lib}/${file}) JRN(${jrnLib}/${jrn}) IMAGES(${images})`;
|
|
2307
2717
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
2308
|
-
return
|
|
2718
|
+
return commandOutcome({
|
|
2309
2719
|
ok: cmdResult.code === 0,
|
|
2310
2720
|
command,
|
|
2311
2721
|
...cmdResult
|
|
@@ -2313,12 +2723,12 @@ export async function handleTool(ctx, name, args) {
|
|
|
2313
2723
|
}
|
|
2314
2724
|
case "ibmi.journal.endPf": {
|
|
2315
2725
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
2316
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2726
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
2317
2727
|
const lib = requireIbmObjectName(args.library, "library");
|
|
2318
2728
|
const file = requireIbmObjectName(args.file, "file");
|
|
2319
2729
|
const command = `ENDJRNPF FILE(${lib}/${file})`;
|
|
2320
2730
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
2321
|
-
return
|
|
2731
|
+
return commandOutcome({
|
|
2322
2732
|
ok: cmdResult.code === 0,
|
|
2323
2733
|
command,
|
|
2324
2734
|
...cmdResult
|
|
@@ -2326,14 +2736,14 @@ export async function handleTool(ctx, name, args) {
|
|
|
2326
2736
|
}
|
|
2327
2737
|
case "ibmi.journal.startIfs": {
|
|
2328
2738
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
2329
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2739
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
2330
2740
|
const objectType = requireIfsJournalObjectType(args?.objectType);
|
|
2331
2741
|
const journalLibrary = requireIbmObjectName(args.journalLibrary, "journalLibrary");
|
|
2332
2742
|
const journal = requireIbmObjectName(args.journal, "journal");
|
|
2333
2743
|
const objectPath = requireIfsPath(args.path, "path");
|
|
2334
2744
|
const command = `STRJRNOBJ OBJ((${quoteClString(objectPath)} ${objectType})) JRN(${journalLibrary}/${journal})`;
|
|
2335
2745
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
2336
|
-
return
|
|
2746
|
+
return commandOutcome({
|
|
2337
2747
|
ok: cmdResult.code === 0,
|
|
2338
2748
|
command,
|
|
2339
2749
|
...cmdResult
|
|
@@ -2341,12 +2751,12 @@ export async function handleTool(ctx, name, args) {
|
|
|
2341
2751
|
}
|
|
2342
2752
|
case "ibmi.journal.endIfs": {
|
|
2343
2753
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
2344
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2754
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
2345
2755
|
const objectType = requireIfsJournalObjectType(args?.objectType);
|
|
2346
2756
|
const objectPath = requireIfsPath(args.path, "path");
|
|
2347
2757
|
const command = `ENDJRNOBJ OBJ((${quoteClString(objectPath)} ${objectType}))`;
|
|
2348
2758
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
2349
|
-
return
|
|
2759
|
+
return commandOutcome({
|
|
2350
2760
|
ok: cmdResult.code === 0,
|
|
2351
2761
|
command,
|
|
2352
2762
|
...cmdResult
|
|
@@ -2358,9 +2768,11 @@ export async function handleTool(ctx, name, args) {
|
|
|
2358
2768
|
const journal = requireIbmObjectName(args.journal, "journal");
|
|
2359
2769
|
const retentionDays = clampNumber(args?.retentionDays, 30, 1, 36500);
|
|
2360
2770
|
const maxDeletes = clampNumber(args?.maxDeletes, 50, 1, 500);
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2771
|
+
// The schema documents dryRun as defaulting to true; only an explicit
|
|
2772
|
+
// dryRun=false may delete receivers.
|
|
2773
|
+
const dryRun = args?.dryRun === undefined || args?.dryRun === null ? true : Boolean(args.dryRun);
|
|
2774
|
+
if (!dryRun) {
|
|
2775
|
+
await requireDangerousApproval(ctx, "journal.receivers.delete", args, "ibmi.journal.receivers.retention requires approve=true unless dryRun=true.");
|
|
2364
2776
|
}
|
|
2365
2777
|
const plan = await planJournalReceiverRetention(conn, {
|
|
2366
2778
|
journalLibrary,
|
|
@@ -2378,7 +2790,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
2378
2790
|
candidates
|
|
2379
2791
|
});
|
|
2380
2792
|
}
|
|
2381
|
-
enforceWritable(ctx, conn, "qsys.write", args);
|
|
2793
|
+
await enforceWritable(ctx, conn, "qsys.write", args);
|
|
2382
2794
|
const deleted = [];
|
|
2383
2795
|
for (const candidate of candidates) {
|
|
2384
2796
|
const command = `DLTJRNRCV JRNRCV(${candidate.receiverLibrary}/${candidate.receiver}) DLTOPT(*IGNINQMSG)`;
|
|
@@ -2390,7 +2802,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
2390
2802
|
error: result.code === 0 ? undefined : String(result.stderr || "")
|
|
2391
2803
|
});
|
|
2392
2804
|
}
|
|
2393
|
-
return
|
|
2805
|
+
return commandOutcome({
|
|
2394
2806
|
ok: deleted.every(item => item.ok),
|
|
2395
2807
|
dryRun: false,
|
|
2396
2808
|
retentionDays,
|
|
@@ -2416,7 +2828,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
2416
2828
|
if (!args?.outputPath) {
|
|
2417
2829
|
return json(report);
|
|
2418
2830
|
}
|
|
2419
|
-
const outputPath =
|
|
2831
|
+
const outputPath = await guardLocalPath(ctx, args.outputPath, "write");
|
|
2420
2832
|
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
2421
2833
|
await fs.writeFile(outputPath, JSON.stringify(report, null, 2), "utf8");
|
|
2422
2834
|
let signaturePath;
|
|
@@ -2444,116 +2856,6 @@ export async function handleTool(ctx, name, args) {
|
|
|
2444
2856
|
signature
|
|
2445
2857
|
});
|
|
2446
2858
|
}
|
|
2447
|
-
case "ibmi.tn5250.connect": {
|
|
2448
|
-
const name = getConnectionName(ctx, args);
|
|
2449
|
-
await ctx.ensureActive(name);
|
|
2450
|
-
const session = ensureTn5250Session(name);
|
|
2451
|
-
appendTn5250Message(session, `Session ready for ${name}.`);
|
|
2452
|
-
return json(session.screen);
|
|
2453
|
-
}
|
|
2454
|
-
case "ibmi.tn5250.readScreen": {
|
|
2455
|
-
const session = requireTn5250Session(ctx, args);
|
|
2456
|
-
return json(session.screen);
|
|
2457
|
-
}
|
|
2458
|
-
case "ibmi.tn5250.setField": {
|
|
2459
|
-
const session = requireTn5250Session(ctx, args);
|
|
2460
|
-
const field = findTn5250Field(session, args.fieldId);
|
|
2461
|
-
if (!field)
|
|
2462
|
-
throw new Error(`TN5250 field not found: ${args.fieldId}`);
|
|
2463
|
-
if (field.protected)
|
|
2464
|
-
throw new Error(`TN5250 field is protected: ${field.id}`);
|
|
2465
|
-
const value = String(args.value || "");
|
|
2466
|
-
field.value = value.length > field.length ? value.substring(0, field.length) : value;
|
|
2467
|
-
touchTn5250Session(session);
|
|
2468
|
-
return json(session.screen);
|
|
2469
|
-
}
|
|
2470
|
-
case "ibmi.tn5250.sendKeys": {
|
|
2471
|
-
const session = requireTn5250Session(ctx, args);
|
|
2472
|
-
const conn = await ctx.ensureActive(session.connectionName);
|
|
2473
|
-
const keys = parseTn5250Keys(args.keys);
|
|
2474
|
-
const timeoutMs = clampNumber(args?.timeoutMs, 45000, 1000, 300000);
|
|
2475
|
-
const commandField = findTn5250Field(session, "command");
|
|
2476
|
-
session.screen.lastKeys = keys;
|
|
2477
|
-
session.screen.status = "busy";
|
|
2478
|
-
touchTn5250Session(session);
|
|
2479
|
-
for (const key of keys) {
|
|
2480
|
-
if (key === "ENTER" || key === "RETURN") {
|
|
2481
|
-
const command = String(commandField?.value || "").trim();
|
|
2482
|
-
if (!command) {
|
|
2483
|
-
appendTn5250Message(session, "No command entered.");
|
|
2484
|
-
continue;
|
|
2485
|
-
}
|
|
2486
|
-
enforcePolicyOperation(ctx, "cl.run", {
|
|
2487
|
-
...args,
|
|
2488
|
-
command,
|
|
2489
|
-
connectionName: session.connectionName
|
|
2490
|
-
});
|
|
2491
|
-
const cmdResult = await runClCommand(conn, command, "ile", undefined, timeoutMs);
|
|
2492
|
-
const recent = await readRecentJoblogMessages(conn, 5);
|
|
2493
|
-
const messages = recent.map((row) => {
|
|
2494
|
-
const id = String(row.MESSAGE_ID || "");
|
|
2495
|
-
const text = String(row.MESSAGE_TEXT || "");
|
|
2496
|
-
return id ? `[${id}] ${text}` : text;
|
|
2497
|
-
});
|
|
2498
|
-
session.screen.commandHistory.unshift({
|
|
2499
|
-
at: new Date().toISOString(),
|
|
2500
|
-
command,
|
|
2501
|
-
ok: cmdResult.code === 0,
|
|
2502
|
-
code: cmdResult.code,
|
|
2503
|
-
stderr: cmdResult.stderr ? String(cmdResult.stderr) : undefined,
|
|
2504
|
-
messages: messages.slice(0, 5)
|
|
2505
|
-
});
|
|
2506
|
-
if (session.screen.commandHistory.length > TN5250_MAX_HISTORY) {
|
|
2507
|
-
session.screen.commandHistory = session.screen.commandHistory.slice(0, TN5250_MAX_HISTORY);
|
|
2508
|
-
}
|
|
2509
|
-
appendTn5250Message(session, `${cmdResult.code === 0 ? "OK" : "ERROR"}: ${command}`);
|
|
2510
|
-
for (const line of messages.slice(0, 3)) {
|
|
2511
|
-
appendTn5250Message(session, line);
|
|
2512
|
-
}
|
|
2513
|
-
if (commandField)
|
|
2514
|
-
commandField.value = "";
|
|
2515
|
-
continue;
|
|
2516
|
-
}
|
|
2517
|
-
if (key === "CLEAR" || key === "F3" || key === "F12") {
|
|
2518
|
-
resetTn5250Screen(session, `Cleared via ${key}.`);
|
|
2519
|
-
continue;
|
|
2520
|
-
}
|
|
2521
|
-
if (key === "TAB") {
|
|
2522
|
-
rotateTn5250Cursor(session);
|
|
2523
|
-
continue;
|
|
2524
|
-
}
|
|
2525
|
-
appendTn5250Message(session, `Unsupported key: ${key}`);
|
|
2526
|
-
}
|
|
2527
|
-
session.screen.status = "ready";
|
|
2528
|
-
touchTn5250Session(session);
|
|
2529
|
-
return json(session.screen);
|
|
2530
|
-
}
|
|
2531
|
-
case "ibmi.tn5250.waitFor": {
|
|
2532
|
-
const session = requireTn5250Session(ctx, args);
|
|
2533
|
-
const timeoutMs = clampNumber(args?.timeoutMs, 5000, 250, 120000);
|
|
2534
|
-
const pattern = args?.text ? String(args.text) : "";
|
|
2535
|
-
if (!pattern) {
|
|
2536
|
-
return json({ matched: true, timeoutMs, screen: session.screen });
|
|
2537
|
-
}
|
|
2538
|
-
const matched = await waitForTn5250Text(session, pattern, timeoutMs);
|
|
2539
|
-
return json({ matched, text: pattern, timeoutMs, screen: session.screen });
|
|
2540
|
-
}
|
|
2541
|
-
case "ibmi.tn5250.snapshot": {
|
|
2542
|
-
const session = requireTn5250Session(ctx, args);
|
|
2543
|
-
return json({
|
|
2544
|
-
capturedAt: new Date().toISOString(),
|
|
2545
|
-
connectionName: session.connectionName,
|
|
2546
|
-
plainText: renderTn5250Screen(session.screen),
|
|
2547
|
-
screen: session.screen
|
|
2548
|
-
});
|
|
2549
|
-
}
|
|
2550
|
-
case "ibmi.tn5250.disconnect": {
|
|
2551
|
-
const name = resolveTn5250SessionName(ctx, args);
|
|
2552
|
-
if (!name)
|
|
2553
|
-
throw new Error("connectionName is required (no active session)");
|
|
2554
|
-
const existed = tn5250Sessions.delete(name);
|
|
2555
|
-
return json({ disconnected: existed, connectionName: name });
|
|
2556
|
-
}
|
|
2557
2859
|
}
|
|
2558
2860
|
throw new Error(`Unknown tool: ${name}`);
|
|
2559
2861
|
}
|
|
@@ -2562,7 +2864,7 @@ async function listActions(ctx) {
|
|
|
2562
2864
|
const custom = await ctx.store.listActions();
|
|
2563
2865
|
return [...builtins, ...custom];
|
|
2564
2866
|
}
|
|
2565
|
-
async function resolveAction(ctx, args) {
|
|
2867
|
+
export async function resolveAction(ctx, args) {
|
|
2566
2868
|
if (args.action)
|
|
2567
2869
|
return args.action;
|
|
2568
2870
|
if (!args.actionName)
|
|
@@ -2570,7 +2872,7 @@ async function resolveAction(ctx, args) {
|
|
|
2570
2872
|
const actions = await listActions(ctx);
|
|
2571
2873
|
return actions.find(a => a.name === args.actionName);
|
|
2572
2874
|
}
|
|
2573
|
-
async function runClCommand(conn, command, environment, cwd, timeoutMs) {
|
|
2875
|
+
export async function runClCommand(conn, command, environment, cwd, timeoutMs) {
|
|
2574
2876
|
if (environment === "pase") {
|
|
2575
2877
|
return await withTimeout(conn.sendCommand({ command, directory: cwd }), timeoutMs, `CL command timed out after ${timeoutMs}ms`);
|
|
2576
2878
|
}
|
|
@@ -2578,22 +2880,16 @@ async function runClCommand(conn, command, environment, cwd, timeoutMs) {
|
|
|
2578
2880
|
return await withTimeout(conn.sendQsh({ command, directory: cwd }), timeoutMs, `CL command timed out after ${timeoutMs}ms`);
|
|
2579
2881
|
}
|
|
2580
2882
|
try {
|
|
2581
|
-
await
|
|
2883
|
+
await runSqlWithTimeout(conn, `CALL QSYS2.QCMDEXC(${Tools.sqlString(command)})`, timeoutMs, `CL command timed out after ${timeoutMs}ms`);
|
|
2582
2884
|
return { code: 0, signal: null, stdout: "", stderr: "", command };
|
|
2583
2885
|
}
|
|
2584
2886
|
catch (err) {
|
|
2585
2887
|
return { code: 1, signal: null, stdout: "", stderr: err?.message || String(err), command };
|
|
2586
2888
|
}
|
|
2587
2889
|
}
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
return await conn.runSQL(`select message_id, message_text, severity, message_timestamp from table(qsys2.joblog_info('*')) order by message_timestamp desc fetch first ${capped} rows only`);
|
|
2592
|
-
}
|
|
2593
|
-
catch {
|
|
2594
|
-
return [];
|
|
2595
|
-
}
|
|
2596
|
-
}
|
|
2890
|
+
// Every filter is applied on the IBM i (table function parameters and WHERE
|
|
2891
|
+
// predicates) before FETCH FIRST, so the row cap never hides matching entries.
|
|
2892
|
+
// Rows come back newest first.
|
|
2597
2893
|
async function queryJournalEntries(conn, options) {
|
|
2598
2894
|
let rows;
|
|
2599
2895
|
try {
|
|
@@ -2602,42 +2898,7 @@ async function queryJournalEntries(conn, options) {
|
|
|
2602
2898
|
catch {
|
|
2603
2899
|
rows = await queryJournalEntriesViaOutfile(conn, options);
|
|
2604
2900
|
}
|
|
2605
|
-
|
|
2606
|
-
if (options.objectLibrary) {
|
|
2607
|
-
normalized = normalized.filter(row => (row.objectLibrary || "") === options.objectLibrary);
|
|
2608
|
-
}
|
|
2609
|
-
if (options.objectName) {
|
|
2610
|
-
normalized = normalized.filter(row => (row.objectName || "") === options.objectName);
|
|
2611
|
-
}
|
|
2612
|
-
if (options.journalCode) {
|
|
2613
|
-
normalized = normalized.filter(row => (row.journalCode || "") === options.journalCode);
|
|
2614
|
-
}
|
|
2615
|
-
if (options.entryType) {
|
|
2616
|
-
normalized = normalized.filter(row => (row.entryType || "") === options.entryType);
|
|
2617
|
-
}
|
|
2618
|
-
if (options.userName) {
|
|
2619
|
-
normalized = normalized.filter(row => (row.userName || "") === options.userName);
|
|
2620
|
-
}
|
|
2621
|
-
if (options.jobName) {
|
|
2622
|
-
normalized = normalized.filter(row => (row.jobName || "").includes(options.jobName));
|
|
2623
|
-
}
|
|
2624
|
-
if (options.programName) {
|
|
2625
|
-
normalized = normalized.filter(row => (row.programName || "") === options.programName);
|
|
2626
|
-
}
|
|
2627
|
-
if (typeof options.sinceSequence === "number") {
|
|
2628
|
-
normalized = normalized.filter(row => typeof row.sequence === "number" && row.sequence >= options.sinceSequence);
|
|
2629
|
-
}
|
|
2630
|
-
if (options.sinceTimestamp) {
|
|
2631
|
-
const cutoff = Date.parse(options.sinceTimestamp);
|
|
2632
|
-
if (Number.isFinite(cutoff)) {
|
|
2633
|
-
normalized = normalized.filter(row => {
|
|
2634
|
-
if (!row.timestamp)
|
|
2635
|
-
return false;
|
|
2636
|
-
const value = Date.parse(row.timestamp);
|
|
2637
|
-
return Number.isFinite(value) && value >= cutoff;
|
|
2638
|
-
});
|
|
2639
|
-
}
|
|
2640
|
-
}
|
|
2901
|
+
const normalized = rows.map(normalizeJournalEntry);
|
|
2641
2902
|
normalized.sort((a, b) => {
|
|
2642
2903
|
const aSeq = typeof a.sequence === "number" ? a.sequence : -1;
|
|
2643
2904
|
const bSeq = typeof b.sequence === "number" ? b.sequence : -1;
|
|
@@ -2649,6 +2910,27 @@ async function queryJournalEntries(conn, options) {
|
|
|
2649
2910
|
});
|
|
2650
2911
|
return normalized.slice(0, options.limit);
|
|
2651
2912
|
}
|
|
2913
|
+
// Turns an ISO-8601 or IBM i style timestamp into a Db2 for i timestamp
|
|
2914
|
+
// expression in the system's local time. Values with an explicit zone (Z or
|
|
2915
|
+
// +hh:mm) are converted from UTC with CURRENT TIMEZONE; values without a zone
|
|
2916
|
+
// are taken as system local time.
|
|
2917
|
+
function sqlTimestampExpression(value) {
|
|
2918
|
+
const raw = String(value || "").trim();
|
|
2919
|
+
const local = raw.match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T-](\d{2})[:.](\d{2})(?:[:.](\d{2})(?:\.(\d{1,6}))?)?)?$/);
|
|
2920
|
+
if (local) {
|
|
2921
|
+
const [, y, mo, d, h = "00", mi = "00", s = "00", frac = "0"] = local;
|
|
2922
|
+
return `timestamp('${y}-${mo}-${d}-${h}.${mi}.${s}.${frac.padEnd(6, "0")}')`;
|
|
2923
|
+
}
|
|
2924
|
+
if (!/(?:Z|[+-]\d{2}:?\d{2})$/i.test(raw)) {
|
|
2925
|
+
throw new Error(`Invalid sinceTimestamp: ${value}`);
|
|
2926
|
+
}
|
|
2927
|
+
const parsed = Date.parse(raw);
|
|
2928
|
+
if (!Number.isFinite(parsed))
|
|
2929
|
+
throw new Error(`Invalid sinceTimestamp: ${value}`);
|
|
2930
|
+
const iso = new Date(parsed).toISOString();
|
|
2931
|
+
const db2 = `${iso.slice(0, 10)}-${iso.slice(11, 13)}.${iso.slice(14, 16)}.${iso.slice(17, 19)}.${iso.slice(20, 23)}000`;
|
|
2932
|
+
return `(timestamp('${db2}') + current timezone)`;
|
|
2933
|
+
}
|
|
2652
2934
|
async function queryJobs(conn, options) {
|
|
2653
2935
|
const scanLimit = Math.min(options.limit * 5, 5000);
|
|
2654
2936
|
const rows = [];
|
|
@@ -2728,27 +3010,61 @@ function normalizeActiveJobRow(row) {
|
|
|
2728
3010
|
cpuSeconds: toNullableNumber(pickFirst(row, ["CPU_TIME", "TOTAL_CPU_TIME", "ELAPSED_TOTAL_SECONDS"]))
|
|
2729
3011
|
};
|
|
2730
3012
|
}
|
|
3013
|
+
// Newest messages first. The table function form takes QUEUE_LIBRARY and
|
|
3014
|
+
// QUEUE_NAME; the view form is the fallback for releases without it.
|
|
2731
3015
|
async function queryMessageQueueEntries(conn, options) {
|
|
2732
3016
|
let rows;
|
|
2733
3017
|
try {
|
|
2734
|
-
rows = await conn.runSQL(`select
|
|
3018
|
+
rows = await conn.runSQL(`select x.*, hex(x.message_key) as message_key_hex from table(qsys2.message_queue_info(queue_library => ${Tools.sqlString(options.library)}, queue_name => ${Tools.sqlString(options.messageQueue)})) x ` +
|
|
3019
|
+
`order by message_timestamp desc fetch first ${options.limit} rows only`);
|
|
2735
3020
|
}
|
|
2736
3021
|
catch {
|
|
2737
|
-
rows = await conn.runSQL(`select
|
|
3022
|
+
rows = await conn.runSQL(`select x.*, hex(x.message_key) as message_key_hex from qsys2.message_queue_info x where message_queue_name=${Tools.sqlString(options.messageQueue)} and message_queue_library=${Tools.sqlString(options.library)} ` +
|
|
3023
|
+
`order by message_timestamp desc fetch first ${options.limit} rows only`);
|
|
2738
3024
|
}
|
|
2739
3025
|
return rows.map(normalizeMessageQueueEntry);
|
|
2740
3026
|
}
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
`
|
|
2747
|
-
|
|
2748
|
-
`
|
|
2749
|
-
|
|
2750
|
-
`
|
|
2751
|
-
|
|
3027
|
+
// QSYS2.OUTPUT_QUEUE_ENTRIES columns: USER_NAME, OUTPUT_QUEUE_NAME,
|
|
3028
|
+
// OUTPUT_QUEUE_LIBRARY_NAME, JOB_NAME (qualified), CREATE_TIMESTAMP, ...
|
|
3029
|
+
function buildSpoolWhereClause(options) {
|
|
3030
|
+
const predicates = [];
|
|
3031
|
+
if (options.userName)
|
|
3032
|
+
predicates.push(`user_name = ${Tools.sqlString(options.userName)}`);
|
|
3033
|
+
if (options.outputQueue)
|
|
3034
|
+
predicates.push(`output_queue_name = ${Tools.sqlString(options.outputQueue)}`);
|
|
3035
|
+
if (options.outputQueueLibrary)
|
|
3036
|
+
predicates.push(`output_queue_library_name = ${Tools.sqlString(options.outputQueueLibrary)}`);
|
|
3037
|
+
if (options.jobName) {
|
|
3038
|
+
predicates.push(options.jobName.includes("/")
|
|
3039
|
+
? `job_name = ${Tools.sqlString(options.jobName)}`
|
|
3040
|
+
: `job_name like ${Tools.sqlString(`%/${options.jobName}`)}`);
|
|
3041
|
+
}
|
|
3042
|
+
return predicates.length > 0 ? ` where ${predicates.join(" and ")}` : "";
|
|
3043
|
+
}
|
|
3044
|
+
async function querySpoolEntries(conn, options) {
|
|
3045
|
+
const limit = options.limit;
|
|
3046
|
+
const where = buildSpoolWhereClause(options);
|
|
3047
|
+
const queries = [];
|
|
3048
|
+
// OUTPUT_QUEUE_ENTRIES scans every spooled file on the system before the
|
|
3049
|
+
// WHERE applies, which can take minutes. SPOOLED_FILE_INFO filters by user
|
|
3050
|
+
// or job inside the service, so use it first when one of those is given.
|
|
3051
|
+
const qualifiedJob = options.jobName?.includes("/") ? options.jobName : undefined;
|
|
3052
|
+
if (options.userName || qualifiedJob) {
|
|
3053
|
+
const params = [
|
|
3054
|
+
options.userName ? `user_name => ${Tools.sqlString(options.userName)}` : "",
|
|
3055
|
+
qualifiedJob ? `job_name => ${Tools.sqlString(qualifiedJob)}` : ""
|
|
3056
|
+
].filter(Boolean).join(", ");
|
|
3057
|
+
const filters = [
|
|
3058
|
+
options.outputQueue ? `output_queue = ${Tools.sqlString(options.outputQueue)}` : "",
|
|
3059
|
+
options.outputQueueLibrary ? `output_queue_library = ${Tools.sqlString(options.outputQueueLibrary)}` : "",
|
|
3060
|
+
options.jobName && !qualifiedJob ? `job_name = ${Tools.sqlString(options.jobName)}` : ""
|
|
3061
|
+
].filter(Boolean);
|
|
3062
|
+
queries.push(`select * from table(qsys2.spooled_file_info(${params})) x${filters.length ? ` where ${filters.join(" and ")}` : ""} order by creation_timestamp desc fetch first ${limit} rows only`);
|
|
3063
|
+
}
|
|
3064
|
+
queries.push(`select * from qsys2.output_queue_entries${where} order by create_timestamp desc fetch first ${limit} rows only`, `select * from qsys2.output_queue_entries${where} fetch first ${limit} rows only`);
|
|
3065
|
+
if (!where) {
|
|
3066
|
+
queries.push(`select * from table(qsys2.output_queue_entries()) fetch first ${limit} rows only`, `select * from qsys2.spooled_file_info fetch first ${limit} rows only`, `select * from table(qsys2.spooled_file_info()) fetch first ${limit} rows only`);
|
|
3067
|
+
}
|
|
2752
3068
|
let lastError;
|
|
2753
3069
|
for (const sql of queries) {
|
|
2754
3070
|
try {
|
|
@@ -2763,7 +3079,8 @@ async function querySpoolEntries(conn, limit) {
|
|
|
2763
3079
|
function normalizeMessageQueueEntry(row) {
|
|
2764
3080
|
return {
|
|
2765
3081
|
source: "msgq",
|
|
2766
|
-
|
|
3082
|
+
// The raw key is binary; the hex form is what ibmi.msgq.reply accepts.
|
|
3083
|
+
messageKey: toNullableString(pickFirst(row, ["MESSAGE_KEY_HEX", "MSGKEY"])),
|
|
2767
3084
|
messageId: toNullableString(pickFirst(row, ["MESSAGE_ID", "MSGID"])),
|
|
2768
3085
|
messageType: toNullableString(pickFirst(row, ["MESSAGE_TYPE", "MSGTYPE"])),
|
|
2769
3086
|
severity: toNullableNumber(pickFirst(row, ["SEVERITY", "MSGSEV"])),
|
|
@@ -2772,23 +3089,35 @@ function normalizeMessageQueueEntry(row) {
|
|
|
2772
3089
|
sentAt: toNullableString(pickFirst(row, ["MESSAGE_TIMESTAMP", "SENT_TIMESTAMP"]))
|
|
2773
3090
|
};
|
|
2774
3091
|
}
|
|
3092
|
+
// QSYS2.OBJECT_LOCK_INFO is a view; filters go into its WHERE clause so the row
|
|
3093
|
+
// cap applies to matching locks only.
|
|
2775
3094
|
async function queryLockEntries(conn, options) {
|
|
3095
|
+
const base = [];
|
|
3096
|
+
if (options.objectLibrary)
|
|
3097
|
+
base.push(`object_schema = ${Tools.sqlString(options.objectLibrary)}`);
|
|
3098
|
+
if (options.objectName)
|
|
3099
|
+
base.push(`object_name = ${Tools.sqlString(options.objectName)}`);
|
|
3100
|
+
if (options.objectType)
|
|
3101
|
+
base.push(`object_type = ${Tools.sqlString(options.objectType)}`);
|
|
3102
|
+
const memberPredicate = options.member ? `system_table_member = ${Tools.sqlString(options.member)}` : undefined;
|
|
3103
|
+
const whereOf = (predicates) => predicates.length > 0 ? ` where ${predicates.join(" and ")}` : "";
|
|
2776
3104
|
let rows;
|
|
3105
|
+
let filterMemberLocally = false;
|
|
2777
3106
|
try {
|
|
2778
|
-
rows = await conn.runSQL(`select * from
|
|
3107
|
+
rows = await conn.runSQL(`select * from qsys2.object_lock_info${whereOf(memberPredicate ? [...base, memberPredicate] : base)} fetch first ${options.limit} rows only`);
|
|
2779
3108
|
}
|
|
2780
|
-
catch {
|
|
2781
|
-
|
|
3109
|
+
catch (err) {
|
|
3110
|
+
if (!memberPredicate)
|
|
3111
|
+
throw err;
|
|
3112
|
+
// Fallback when the member column is named differently on this release:
|
|
3113
|
+
// keep the object filters in SQL and match the member locally.
|
|
3114
|
+
rows = await conn.runSQL(`select * from qsys2.object_lock_info${whereOf(base)} fetch first 5000 rows only`);
|
|
3115
|
+
filterMemberLocally = true;
|
|
2782
3116
|
}
|
|
2783
3117
|
let normalized = rows.map(normalizeLockEntry);
|
|
2784
|
-
if (options.
|
|
2785
|
-
normalized = normalized.filter(row => (row.
|
|
2786
|
-
|
|
2787
|
-
normalized = normalized.filter(row => (row.objectName || "") === String(options.objectName).toUpperCase());
|
|
2788
|
-
if (options.objectType)
|
|
2789
|
-
normalized = normalized.filter(row => (row.objectType || "") === String(options.objectType).toUpperCase());
|
|
2790
|
-
if (options.member)
|
|
2791
|
-
normalized = normalized.filter(row => (row.member || "") === String(options.member).toUpperCase());
|
|
3118
|
+
if (filterMemberLocally && options.member) {
|
|
3119
|
+
normalized = normalized.filter(row => (row.member || "") === options.member);
|
|
3120
|
+
}
|
|
2792
3121
|
return normalized.slice(0, options.limit);
|
|
2793
3122
|
}
|
|
2794
3123
|
function normalizeLockEntry(row) {
|
|
@@ -2797,7 +3126,7 @@ function normalizeLockEntry(row) {
|
|
|
2797
3126
|
objectLibrary: toNullableString(pickFirst(row, ["OBJECT_SCHEMA", "OBJECT_LIBRARY", "OBJLIB"])),
|
|
2798
3127
|
objectName: toNullableString(pickFirst(row, ["OBJECT_NAME", "OBJNAME"])),
|
|
2799
3128
|
objectType: toNullableString(pickFirst(row, ["OBJECT_TYPE", "OBJTYPE"])),
|
|
2800
|
-
member: toNullableString(pickFirst(row, ["MEMBER_NAME", "MEMBER"])),
|
|
3129
|
+
member: toNullableString(pickFirst(row, ["SYSTEM_TABLE_MEMBER", "MEMBER_NAME", "MEMBER"])),
|
|
2801
3130
|
lockState: toNullableString(pickFirst(row, ["LOCK_STATE", "LOCK_STATUS", "STATUS"])),
|
|
2802
3131
|
lockScope: toNullableString(pickFirst(row, ["LOCK_SCOPE", "SCOPE"])),
|
|
2803
3132
|
lockHolderJob: toNullableString(pickFirst(row, ["JOB_NAME", "LOCK_JOB_NAME"])),
|
|
@@ -2825,23 +3154,27 @@ function normalizeObjectAuthorityRow(row) {
|
|
|
2825
3154
|
raw: row
|
|
2826
3155
|
};
|
|
2827
3156
|
}
|
|
3157
|
+
const DATA_QUEUE_MAX_WAIT_SECONDS = 60;
|
|
3158
|
+
// remove=false never falls back to anything that takes the entry off the
|
|
3159
|
+
// queue: it peeks with RECEIVE_DATA_QUEUE(REMOVE => 'NO') and then with the
|
|
3160
|
+
// read-only DATA_QUEUE_ENTRIES service.
|
|
2828
3161
|
async function queryDataQueueEntries(conn, options) {
|
|
3162
|
+
const queue = Tools.sqlString(options.queue);
|
|
3163
|
+
const library = Tools.sqlString(options.library);
|
|
3164
|
+
const waitSeconds = Math.max(0, Math.min(DATA_QUEUE_MAX_WAIT_SECONDS, Math.floor(options.waitSeconds)));
|
|
2829
3165
|
let rows;
|
|
3166
|
+
if (options.remove) {
|
|
3167
|
+
rows = await conn.runSQL(`select * from table(qsys2.receive_data_queue(data_queue => ${queue}, data_queue_library => ${library}, ` +
|
|
3168
|
+
`wait_time => ${waitSeconds}, remove => 'YES')) fetch first 1 rows only`);
|
|
3169
|
+
return rows.map(normalizeDataQueueEntry);
|
|
3170
|
+
}
|
|
2830
3171
|
try {
|
|
2831
|
-
rows = await conn.runSQL(`select * from table(qsys2.receive_data_queue(${
|
|
3172
|
+
rows = await conn.runSQL(`select * from table(qsys2.receive_data_queue(data_queue => ${queue}, data_queue_library => ${library}, ` +
|
|
3173
|
+
`wait_time => ${waitSeconds}, remove => 'NO')) fetch first 1 rows only`);
|
|
2832
3174
|
}
|
|
2833
3175
|
catch {
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
}
|
|
2837
|
-
catch {
|
|
2838
|
-
try {
|
|
2839
|
-
rows = await conn.runSQL(`select * from table(qsys2.data_queue_entries(${Tools.sqlString(options.queue)}, ${Tools.sqlString(options.library)})) fetch first 1 rows only`);
|
|
2840
|
-
}
|
|
2841
|
-
catch {
|
|
2842
|
-
rows = await conn.runSQL(`select * from qsys2.data_queue_entries where upper(data_queue_name)=${Tools.sqlString(options.queue)} and upper(data_queue_library)=${Tools.sqlString(options.library)} fetch first 1 rows only`);
|
|
2843
|
-
}
|
|
2844
|
-
}
|
|
3176
|
+
rows = await conn.runSQL(`select * from table(qsys2.data_queue_entries(data_queue => ${queue}, data_queue_library => ${library})) ` +
|
|
3177
|
+
`order by ordinal_position fetch first 1 rows only`);
|
|
2845
3178
|
}
|
|
2846
3179
|
return rows.map(normalizeDataQueueEntry);
|
|
2847
3180
|
}
|
|
@@ -2871,7 +3204,8 @@ function normalizeDataAreaInfoRow(row) {
|
|
|
2871
3204
|
dataArea: toNullableString(pickFirst(row, ["DATA_AREA_NAME", "OBJECT_NAME"])),
|
|
2872
3205
|
type: toNullableString(pickFirst(row, ["DATA_AREA_TYPE", "OBJECT_TYPE"])),
|
|
2873
3206
|
length: toNullableNumber(pickFirst(row, ["LENGTH", "DATA_AREA_LENGTH"])),
|
|
2874
|
-
|
|
3207
|
+
decimalPositions: toNullableNumber(pickFirst(row, ["DECIMAL_POSITIONS"])),
|
|
3208
|
+
value: toNullableString(pickFirst(row, ["DATA_AREA_VALUE", "CHARACTER_VALUE", "VALUE", "DECIMAL_VALUE"])),
|
|
2875
3209
|
updatedAt: toNullableString(pickFirst(row, ["LAST_CHANGED_TIMESTAMP", "CHANGE_TIMESTAMP"]))
|
|
2876
3210
|
};
|
|
2877
3211
|
}
|
|
@@ -2884,22 +3218,100 @@ function buildSpoolCommand(commandName, args) {
|
|
|
2884
3218
|
: "*LAST";
|
|
2885
3219
|
return `${commandName} FILE(${spooledFileName}) JOB(${jobName}) SPLNBR(${spooledFileNumber})`;
|
|
2886
3220
|
}
|
|
3221
|
+
function isQualifiedJobName(value) {
|
|
3222
|
+
return /^[^/]+\/[^/]+\/[^/]+$/.test(value);
|
|
3223
|
+
}
|
|
2887
3224
|
async function queryJournalEntriesViaDisplayJournal(conn, options) {
|
|
2888
|
-
|
|
2889
|
-
|
|
3225
|
+
if (options.objectLibrary && !options.objectName) {
|
|
3226
|
+
// A library on its own is not a DISPLAY_JOURNAL filter; the outfile path
|
|
3227
|
+
// filters it in SQL.
|
|
3228
|
+
throw new Error("objectLibrary without objectName is handled by the DSPJRN outfile query");
|
|
3229
|
+
}
|
|
3230
|
+
const params = [
|
|
3231
|
+
`journal_library => ${Tools.sqlString(options.journalLibrary)}`,
|
|
3232
|
+
`journal_name => ${Tools.sqlString(options.journal)}`
|
|
3233
|
+
];
|
|
3234
|
+
const where = [];
|
|
3235
|
+
const since = options.sinceTimestamp ? sqlTimestampExpression(options.sinceTimestamp) : undefined;
|
|
3236
|
+
if (typeof options.sinceSequence === "number" || since) {
|
|
3237
|
+
// Search the whole receiver chain, not only the attached receiver, when a
|
|
3238
|
+
// starting point is given.
|
|
3239
|
+
params.push(`starting_receiver_name => '*CURCHAIN'`);
|
|
3240
|
+
}
|
|
3241
|
+
if (typeof options.sinceSequence === "number") {
|
|
3242
|
+
params.push(`starting_sequence => ${Math.max(0, Math.floor(options.sinceSequence))}`);
|
|
3243
|
+
where.push(`sequence_number >= ${Math.max(0, Math.floor(options.sinceSequence))}`);
|
|
3244
|
+
if (since)
|
|
3245
|
+
where.push(`entry_timestamp >= ${since}`);
|
|
3246
|
+
}
|
|
3247
|
+
else if (since) {
|
|
3248
|
+
params.push(`starting_timestamp => ${since}`);
|
|
3249
|
+
}
|
|
3250
|
+
if (options.journalCode)
|
|
3251
|
+
params.push(`journal_codes => ${Tools.sqlString(options.journalCode)}`);
|
|
3252
|
+
if (options.entryType)
|
|
3253
|
+
params.push(`journal_entry_types => ${Tools.sqlString(options.entryType)}`);
|
|
3254
|
+
if (options.objectName) {
|
|
3255
|
+
params.push(`object_library => ${Tools.sqlString(options.objectLibrary || "*LIBL")}`);
|
|
3256
|
+
params.push(`object_name => ${Tools.sqlString(options.objectName)}`);
|
|
3257
|
+
const objectType = options.objectType || "*FILE";
|
|
3258
|
+
params.push(`object_objtype => ${Tools.sqlString(objectType)}`);
|
|
3259
|
+
if (objectType === "*FILE")
|
|
3260
|
+
params.push(`object_member => '*ALL'`);
|
|
3261
|
+
}
|
|
3262
|
+
if (options.userName)
|
|
3263
|
+
params.push(`user => ${Tools.sqlString(options.userName)}`);
|
|
3264
|
+
if (options.programName)
|
|
3265
|
+
params.push(`program => ${Tools.sqlString(options.programName)}`);
|
|
3266
|
+
if (options.jobName) {
|
|
3267
|
+
if (isQualifiedJobName(options.jobName))
|
|
3268
|
+
params.push(`job => ${Tools.sqlString(options.jobName)}`);
|
|
3269
|
+
else
|
|
3270
|
+
where.push(`job_name = ${Tools.sqlString(options.jobName)}`);
|
|
3271
|
+
}
|
|
3272
|
+
const whereClause = where.length > 0 ? ` where ${where.join(" and ")}` : "";
|
|
3273
|
+
const rows = await conn.runSQL(`select * from table(qsys2.display_journal(${params.join(", ")}))${whereClause} ` +
|
|
3274
|
+
`order by sequence_number desc fetch first ${options.limit} rows only`);
|
|
2890
3275
|
return rows;
|
|
2891
3276
|
}
|
|
3277
|
+
// DSPJRN writes to QTEMP, which belongs to one job. The CALL QCMDEXC and the
|
|
3278
|
+
// SELECT are sent in the same QZDFMDB2 request so they run in the same job.
|
|
2892
3279
|
async function queryJournalEntriesViaOutfile(conn, options) {
|
|
2893
|
-
const outfile = `MCPJ${String(Math.floor(Math.random() *
|
|
2894
|
-
const
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
const
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
3280
|
+
const outfile = `MCPJ${String(Math.floor(Math.random() * 900000) + 100000)}`;
|
|
3281
|
+
const rangeOption = typeof options.sinceSequence === "number" || options.sinceTimestamp ? " RCVRNG(*CURCHAIN)" : "";
|
|
3282
|
+
const command = `DSPJRN JRN(${options.journalLibrary}/${options.journal})${rangeOption} ` +
|
|
3283
|
+
`OUTPUT(*OUTFILE) OUTFILFMT(*TYPE5) OUTFILE(QTEMP/${outfile})`;
|
|
3284
|
+
const where = [];
|
|
3285
|
+
if (options.objectLibrary)
|
|
3286
|
+
where.push(`JOLIB = ${Tools.sqlString(options.objectLibrary)}`);
|
|
3287
|
+
if (options.objectName)
|
|
3288
|
+
where.push(`JOOBJ = ${Tools.sqlString(options.objectName)}`);
|
|
3289
|
+
if (options.journalCode)
|
|
3290
|
+
where.push(`JOCODE = ${Tools.sqlString(options.journalCode)}`);
|
|
3291
|
+
if (options.entryType)
|
|
3292
|
+
where.push(`JOENTT = ${Tools.sqlString(options.entryType)}`);
|
|
3293
|
+
if (options.userName)
|
|
3294
|
+
where.push(`JOUSPF = ${Tools.sqlString(options.userName)}`);
|
|
3295
|
+
if (options.programName)
|
|
3296
|
+
where.push(`JOPGM = ${Tools.sqlString(options.programName)}`);
|
|
3297
|
+
if (options.jobName) {
|
|
3298
|
+
if (isQualifiedJobName(options.jobName)) {
|
|
3299
|
+
const [number, user, name] = options.jobName.split("/");
|
|
3300
|
+
where.push(`JONBR = ${Tools.sqlString(number)}`, `JOUSER = ${Tools.sqlString(user)}`, `JOJOB = ${Tools.sqlString(name)}`);
|
|
3301
|
+
}
|
|
3302
|
+
else {
|
|
3303
|
+
where.push(`JOJOB = ${Tools.sqlString(options.jobName)}`);
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
if (typeof options.sinceSequence === "number")
|
|
3307
|
+
where.push(`JOSEQN >= ${Math.max(0, Math.floor(options.sinceSequence))}`);
|
|
3308
|
+
if (options.sinceTimestamp)
|
|
3309
|
+
where.push(`JOTSTP >= ${sqlTimestampExpression(options.sinceTimestamp)}`);
|
|
3310
|
+
const whereClause = where.length > 0 ? ` where ${where.join(" and ")}` : "";
|
|
3311
|
+
const rows = await conn.runSQL([
|
|
3312
|
+
`CALL QSYS2.QCMDEXC(${Tools.sqlString(command)});`,
|
|
3313
|
+
`select * from QTEMP.${outfile}${whereClause} order by JOSEQN desc fetch first ${options.limit} rows only;`
|
|
3314
|
+
].join("\n"));
|
|
2903
3315
|
return rows;
|
|
2904
3316
|
}
|
|
2905
3317
|
function normalizeJournalEntry(row) {
|
|
@@ -2911,7 +3323,7 @@ function normalizeJournalEntry(row) {
|
|
|
2911
3323
|
const objectName = toNullableString(pickFirst(row, ["OBJECT_NAME", "JOOBJ", "OBJNAME"]));
|
|
2912
3324
|
const objectType = toNullableString(pickFirst(row, ["OBJECT_TYPE", "JOOBJT", "OBJTYPE"]));
|
|
2913
3325
|
const jobName = toNullableString(pickFirst(row, ["JOB_NAME", "JOJOB", "JOB"]));
|
|
2914
|
-
const userName = toNullableString(pickFirst(row, ["USER_NAME", "JOUSER", "USER"]));
|
|
3326
|
+
const userName = toNullableString(pickFirst(row, ["USER_NAME", "CURRENT_USER", "JOUSPF", "JOUSER", "USER"]));
|
|
2915
3327
|
const programName = toNullableString(pickFirst(row, ["PROGRAM_NAME", "JOPGM", "PROGRAM"]));
|
|
2916
3328
|
return {
|
|
2917
3329
|
source: "journal",
|
|
@@ -2927,143 +3339,355 @@ function normalizeJournalEntry(row) {
|
|
|
2927
3339
|
programName
|
|
2928
3340
|
};
|
|
2929
3341
|
}
|
|
2930
|
-
function pickFirst(row, keys) {
|
|
3342
|
+
export function pickFirst(row, keys) {
|
|
2931
3343
|
for (const key of keys) {
|
|
2932
3344
|
if (typeof row[key] !== "undefined" && row[key] !== null)
|
|
2933
3345
|
return row[key];
|
|
2934
3346
|
}
|
|
2935
3347
|
return undefined;
|
|
2936
3348
|
}
|
|
2937
|
-
function toNullableNumber(value) {
|
|
3349
|
+
export function toNullableNumber(value) {
|
|
2938
3350
|
const n = Number(value);
|
|
2939
3351
|
return Number.isFinite(n) ? n : null;
|
|
2940
3352
|
}
|
|
2941
|
-
function toNullableString(value) {
|
|
3353
|
+
export function toNullableString(value) {
|
|
2942
3354
|
if (typeof value === "undefined" || value === null)
|
|
2943
3355
|
return null;
|
|
2944
3356
|
const text = String(value).trim();
|
|
2945
3357
|
return text.length > 0 ? text : null;
|
|
2946
3358
|
}
|
|
2947
|
-
function
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
3359
|
+
function hasMemberListExtensions(args) {
|
|
3360
|
+
return args?.responseFormat === "page" || [
|
|
3361
|
+
"member", "memberPrefix", "memberContains", "sourceTypes", "changedSince", "limit", "cursor"
|
|
3362
|
+
].some(key => typeof args?.[key] !== "undefined");
|
|
3363
|
+
}
|
|
3364
|
+
function hasStructuredMemberReadOptions(args) {
|
|
3365
|
+
return args?.responseFormat === "structured" || [
|
|
3366
|
+
"startLine", "endLine", "ranges", "before", "after", "metadataOnly",
|
|
3367
|
+
"expectedVersion", "expectedSha256", "forceFresh"
|
|
3368
|
+
].some(key => typeof args?.[key] !== "undefined");
|
|
3369
|
+
}
|
|
3370
|
+
function memberVersionConflict(snapshot, args) {
|
|
3371
|
+
const versionMismatch = args?.expectedVersion && args.expectedVersion !== snapshot.version;
|
|
3372
|
+
const hashMismatch = args?.expectedSha256 && String(args.expectedSha256).toLowerCase() !== snapshot.sha256.toLowerCase();
|
|
3373
|
+
if (!versionMismatch && !hashMismatch)
|
|
3374
|
+
return undefined;
|
|
3375
|
+
const { identity } = snapshot;
|
|
3376
|
+
return toolError(`Version conflict: ${identity.library}/${identity.sourceFile}(${identity.member}) is now version ${snapshot.version}. ` +
|
|
3377
|
+
"Nothing was modified. Re-read the member and retry with the current version.", {
|
|
3378
|
+
code: "VERSION_CONFLICT",
|
|
3379
|
+
status: "version-conflict",
|
|
3380
|
+
identity,
|
|
3381
|
+
expectedVersion: args?.expectedVersion,
|
|
3382
|
+
expectedSha256: args?.expectedSha256,
|
|
3383
|
+
actualVersion: snapshot.version,
|
|
3384
|
+
actualSha256: snapshot.sha256,
|
|
3385
|
+
currentVersion: snapshot.version,
|
|
3386
|
+
currentSha256: snapshot.sha256,
|
|
3387
|
+
observedAt: snapshot.observedAt,
|
|
3388
|
+
modified: false
|
|
3389
|
+
});
|
|
3390
|
+
}
|
|
3391
|
+
const DEFAULT_SOURCE_READ_MAX_BYTES = 2_000_000;
|
|
3392
|
+
const DEFAULT_SOURCE_READ_MAX_LINES = 50_000;
|
|
3393
|
+
const MAX_RANGE_LINES = 2_000;
|
|
3394
|
+
function sourceReadLimits(conn) {
|
|
3395
|
+
const config = typeof conn?.getConfig === "function" ? conn.getConfig() || {} : {};
|
|
3396
|
+
const read = (value, fallback) => {
|
|
3397
|
+
const parsed = Number(value);
|
|
3398
|
+
if (value === undefined || value === null || value === "" || !Number.isFinite(parsed) || parsed < 0)
|
|
3399
|
+
return fallback;
|
|
3400
|
+
return Math.floor(parsed);
|
|
3401
|
+
};
|
|
3402
|
+
return {
|
|
3403
|
+
maxBytes: read(config.sourceReadMaxBytes, DEFAULT_SOURCE_READ_MAX_BYTES),
|
|
3404
|
+
maxLines: read(config.sourceReadMaxLines, DEFAULT_SOURCE_READ_MAX_LINES)
|
|
3405
|
+
};
|
|
3406
|
+
}
|
|
3407
|
+
function isFullMemberRead(args) {
|
|
3408
|
+
return !args?.metadataOnly &&
|
|
3409
|
+
typeof args?.startLine === "undefined" &&
|
|
3410
|
+
typeof args?.endLine === "undefined" &&
|
|
3411
|
+
!(Array.isArray(args?.ranges) && args.ranges.length > 0);
|
|
3412
|
+
}
|
|
3413
|
+
function exceedsSourceLimits(content, lineCount, limits) {
|
|
3414
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
3415
|
+
const lines = typeof lineCount === "number" ? lineCount : (content === "" ? 0 : content.split("\n").length);
|
|
3416
|
+
const tooLarge = (limits.maxBytes > 0 && bytes > limits.maxBytes) || (limits.maxLines > 0 && lines > limits.maxLines);
|
|
3417
|
+
return tooLarge ? { bytes, lineCount: lines } : undefined;
|
|
3418
|
+
}
|
|
3419
|
+
function sourceTooLarge(content, lineCount, limits, args) {
|
|
3420
|
+
const size = exceedsSourceLimits(content, lineCount, limits);
|
|
3421
|
+
return size ? sourceTooLargeError(args, size, limits) : undefined;
|
|
3422
|
+
}
|
|
3423
|
+
function sourceTooLargeError(args, size, limits, extra = {}) {
|
|
3424
|
+
const name = `${String(args?.library || "").toUpperCase()}/${String(args?.sourceFile || "").toUpperCase()}(${String(args?.member || "").toUpperCase()})`;
|
|
3425
|
+
return toolError(`${name} is too large for a full read (${size.lineCount ?? "?"} lines` +
|
|
3426
|
+
`${typeof size.bytes === "number" ? `, ${size.bytes} bytes` : ""}; limits ${limits.maxLines} lines, ${limits.maxBytes} bytes). ` +
|
|
3427
|
+
"Read it in pieces with startLine/endLine or ranges, locate code with ibmi.search.members or ibmi.source.inspect " +
|
|
3428
|
+
"(search, outline), or use metadataOnly: true for its size and version.", { code: "SOURCE_TOO_LARGE", ...size, maxLines: limits.maxLines, maxBytes: limits.maxBytes, ...extra });
|
|
3429
|
+
}
|
|
3430
|
+
function formatMemberReadResult(snapshot, args, limits) {
|
|
3431
|
+
if (isFullMemberRead(args)) {
|
|
3432
|
+
const size = exceedsSourceLimits(snapshot.content, snapshot.lines.length, limits);
|
|
3433
|
+
if (size) {
|
|
3434
|
+
return sourceTooLargeError(args, size, limits, {
|
|
3435
|
+
identity: snapshot.identity,
|
|
3436
|
+
metadata: snapshot.metadata,
|
|
3437
|
+
version: snapshot.version,
|
|
3438
|
+
sha256: snapshot.sha256,
|
|
3439
|
+
observedAt: snapshot.observedAt
|
|
3440
|
+
});
|
|
2982
3441
|
}
|
|
3442
|
+
}
|
|
3443
|
+
return json(formatMemberRead(snapshot, args, limits));
|
|
3444
|
+
}
|
|
3445
|
+
function memberMetadataEnvelope(args, metadata, observedAt) {
|
|
3446
|
+
return {
|
|
3447
|
+
status: "found",
|
|
3448
|
+
identity: {
|
|
3449
|
+
library: String(args.library).toUpperCase(),
|
|
3450
|
+
sourceFile: String(args.sourceFile).toUpperCase(),
|
|
3451
|
+
member: String(args.member).toUpperCase()
|
|
3452
|
+
},
|
|
3453
|
+
metadata,
|
|
3454
|
+
...(metadata?.lastChanged ? { changeMarker: metadata.lastChanged } : {}),
|
|
3455
|
+
observedAt,
|
|
3456
|
+
contentLoaded: false,
|
|
3457
|
+
cacheStatus: "bypass",
|
|
3458
|
+
warnings: [
|
|
3459
|
+
"Content was not downloaded, so sha256 and version are omitted. Pass expectedVersion/expectedSha256 or read without metadataOnly to compute them."
|
|
3460
|
+
]
|
|
2983
3461
|
};
|
|
2984
|
-
tn5250Sessions.set(connectionName, session);
|
|
2985
|
-
return session;
|
|
2986
3462
|
}
|
|
2987
|
-
function
|
|
2988
|
-
const
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
3463
|
+
async function describeWrittenMember(conn, args, previous) {
|
|
3464
|
+
const identity = {
|
|
3465
|
+
library: String(args.library).toUpperCase(),
|
|
3466
|
+
sourceFile: String(args.sourceFile).toUpperCase(),
|
|
3467
|
+
member: String(args.member).toUpperCase()
|
|
3468
|
+
};
|
|
3469
|
+
const base = {
|
|
3470
|
+
status: "written",
|
|
3471
|
+
identity,
|
|
3472
|
+
modified: true,
|
|
3473
|
+
...(previous ? { previousVersion: previous.version, previousSha256: previous.sha256 } : {})
|
|
3474
|
+
};
|
|
3475
|
+
try {
|
|
3476
|
+
// Read back what the host stored (record length and CCSID conversion may
|
|
3477
|
+
// differ from what was sent) so the caller can chain the next edit.
|
|
3478
|
+
const snapshot = await conn.content.getMemberSnapshot(args.library, args.sourceFile, args.member, true);
|
|
3479
|
+
return {
|
|
3480
|
+
...base,
|
|
3481
|
+
version: snapshot.version,
|
|
3482
|
+
sha256: snapshot.sha256,
|
|
3483
|
+
lineCount: snapshot.lines.length,
|
|
3484
|
+
observedAt: snapshot.observedAt
|
|
3485
|
+
};
|
|
3486
|
+
}
|
|
3487
|
+
catch (error) {
|
|
3488
|
+
return { ...base, warnings: [`Write succeeded, but reading back the new version failed: ${error?.message || String(error)}`] };
|
|
2994
3489
|
}
|
|
2995
|
-
return session;
|
|
2996
3490
|
}
|
|
2997
|
-
function
|
|
2998
|
-
|
|
3491
|
+
function formatMemberRead(snapshot, args, limits) {
|
|
3492
|
+
const envelope = {
|
|
3493
|
+
status: "found",
|
|
3494
|
+
identity: snapshot.identity,
|
|
3495
|
+
metadata: snapshot.metadata,
|
|
3496
|
+
version: snapshot.version,
|
|
3497
|
+
sha256: snapshot.sha256,
|
|
3498
|
+
observedAt: snapshot.observedAt,
|
|
3499
|
+
...(snapshot.validatedAt ? { validatedAt: snapshot.validatedAt } : {}),
|
|
3500
|
+
complete: snapshot.complete,
|
|
3501
|
+
transport: snapshot.transport,
|
|
3502
|
+
cacheStatus: snapshot.cacheStatus,
|
|
3503
|
+
lineCount: snapshot.lines.length,
|
|
3504
|
+
warnings: snapshot.warnings
|
|
3505
|
+
};
|
|
3506
|
+
if (args?.metadataOnly)
|
|
3507
|
+
return envelope;
|
|
3508
|
+
const selection = normalizeLineRanges(args, snapshot.lines.length);
|
|
3509
|
+
if (!selection.requested) {
|
|
3510
|
+
const size = limits ? exceedsSourceLimits(snapshot.content, snapshot.lines.length, limits) : undefined;
|
|
3511
|
+
if (size) {
|
|
3512
|
+
// Composite inspect read_ranges without ranges: never flood the caller.
|
|
3513
|
+
return {
|
|
3514
|
+
...envelope,
|
|
3515
|
+
contentOmitted: true,
|
|
3516
|
+
...size,
|
|
3517
|
+
warnings: [...snapshot.warnings, "Member exceeds the full-read size limit; request ranges instead."]
|
|
3518
|
+
};
|
|
3519
|
+
}
|
|
3520
|
+
return { ...envelope, content: snapshot.content };
|
|
3521
|
+
}
|
|
3522
|
+
return {
|
|
3523
|
+
...envelope,
|
|
3524
|
+
warnings: [...snapshot.warnings, ...selection.warnings],
|
|
3525
|
+
...(selection.truncated ? { truncated: true, nextLine: selection.nextLine } : {}),
|
|
3526
|
+
ranges: selection.ranges.map(range => ({
|
|
3527
|
+
start: range.start,
|
|
3528
|
+
end: range.end,
|
|
3529
|
+
lines: snapshot.lines.slice(range.start - 1, range.end).map((content, index) => ({
|
|
3530
|
+
number: range.start + index,
|
|
3531
|
+
content
|
|
3532
|
+
}))
|
|
3533
|
+
}))
|
|
3534
|
+
};
|
|
2999
3535
|
}
|
|
3000
|
-
function
|
|
3001
|
-
const
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3536
|
+
function normalizeLineRanges(args, lineCount) {
|
|
3537
|
+
const before = clampInteger(args?.before, 0, 200, 0);
|
|
3538
|
+
const after = clampInteger(args?.after, 0, 200, 0);
|
|
3539
|
+
const requested = [];
|
|
3540
|
+
if (typeof args?.startLine !== "undefined" || typeof args?.endLine !== "undefined") {
|
|
3541
|
+
const start = typeof args.startLine !== "undefined" ? Number(args.startLine) : 1;
|
|
3542
|
+
// startLine without endLine reads to the end of the member.
|
|
3543
|
+
const end = typeof args.endLine !== "undefined" ? Number(args.endLine) : Math.max(start, lineCount);
|
|
3544
|
+
requested.push({ start, end });
|
|
3545
|
+
}
|
|
3546
|
+
if (Array.isArray(args?.ranges)) {
|
|
3547
|
+
for (const range of args.ranges.slice(0, 8))
|
|
3548
|
+
requested.push({ start: Number(range.start), end: Number(range.end) });
|
|
3549
|
+
}
|
|
3550
|
+
const warnings = [];
|
|
3551
|
+
if (requested.length === 0)
|
|
3552
|
+
return { requested: false, ranges: [], warnings, truncated: false };
|
|
3553
|
+
const normalized = [];
|
|
3554
|
+
for (const range of requested) {
|
|
3555
|
+
if (!Number.isFinite(range.start) || !Number.isFinite(range.end) || range.start < 1 || range.end < range.start) {
|
|
3556
|
+
throw new Error(`Invalid line range ${range.start}-${range.end}`);
|
|
3557
|
+
}
|
|
3558
|
+
const start = Math.max(1, Math.floor(range.start) - before);
|
|
3559
|
+
const end = Math.min(lineCount, Math.floor(range.end) + after);
|
|
3560
|
+
if (start > lineCount || end < start) {
|
|
3561
|
+
warnings.push(`Lines ${range.start}-${range.end} are past the end of the member (${lineCount} lines).`);
|
|
3562
|
+
continue;
|
|
3563
|
+
}
|
|
3564
|
+
normalized.push({ start, end });
|
|
3565
|
+
}
|
|
3566
|
+
normalized.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
3567
|
+
const merged = [];
|
|
3568
|
+
for (const range of normalized) {
|
|
3569
|
+
const previous = merged.at(-1);
|
|
3570
|
+
if (previous && range.start <= previous.end + 1)
|
|
3571
|
+
previous.end = Math.max(previous.end, range.end);
|
|
3572
|
+
else
|
|
3573
|
+
merged.push({ ...range });
|
|
3574
|
+
}
|
|
3575
|
+
// Cap the response instead of failing; nextLine says where to continue.
|
|
3576
|
+
const limited = [];
|
|
3577
|
+
let remaining = MAX_RANGE_LINES;
|
|
3578
|
+
let nextLine;
|
|
3579
|
+
for (const range of merged) {
|
|
3580
|
+
if (remaining <= 0) {
|
|
3581
|
+
nextLine ??= range.start;
|
|
3582
|
+
break;
|
|
3583
|
+
}
|
|
3584
|
+
const length = range.end - range.start + 1;
|
|
3585
|
+
if (length > remaining) {
|
|
3586
|
+
limited.push({ start: range.start, end: range.start + remaining - 1 });
|
|
3587
|
+
nextLine = range.start + remaining;
|
|
3588
|
+
remaining = 0;
|
|
3589
|
+
continue;
|
|
3590
|
+
}
|
|
3591
|
+
limited.push(range);
|
|
3592
|
+
remaining -= length;
|
|
3593
|
+
}
|
|
3594
|
+
if (nextLine !== undefined) {
|
|
3595
|
+
warnings.push(`Response limited to ${MAX_RANGE_LINES} lines; continue from line ${nextLine}.`);
|
|
3596
|
+
}
|
|
3597
|
+
return { requested: true, ranges: limited, warnings, truncated: nextLine !== undefined, nextLine };
|
|
3009
3598
|
}
|
|
3010
|
-
function
|
|
3011
|
-
const
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
}
|
|
3023
|
-
function appendTn5250Message(session, message) {
|
|
3024
|
-
session.screen.textLines.unshift(message);
|
|
3025
|
-
if (session.screen.textLines.length > TN5250_MAX_LINES) {
|
|
3026
|
-
session.screen.textLines = session.screen.textLines.slice(0, TN5250_MAX_LINES);
|
|
3027
|
-
}
|
|
3028
|
-
touchTn5250Session(session);
|
|
3029
|
-
}
|
|
3030
|
-
function resetTn5250Screen(session, message) {
|
|
3031
|
-
const commandField = findTn5250Field(session, "command");
|
|
3032
|
-
if (commandField)
|
|
3033
|
-
commandField.value = "";
|
|
3034
|
-
session.screen.textLines = [message, `Connected to ${session.connectionName}.`];
|
|
3035
|
-
session.screen.cursorFieldId = "command";
|
|
3036
|
-
session.screen.lastKeys = [];
|
|
3037
|
-
touchTn5250Session(session);
|
|
3038
|
-
}
|
|
3039
|
-
function rotateTn5250Cursor(session) {
|
|
3040
|
-
const fields = session.screen.fields;
|
|
3041
|
-
if (fields.length === 0)
|
|
3599
|
+
function normalizeSearchTerms(args) {
|
|
3600
|
+
const terms = [
|
|
3601
|
+
...(typeof args?.term === "string" ? [args.term] : []),
|
|
3602
|
+
...(Array.isArray(args?.terms) ? args.terms : [])
|
|
3603
|
+
].map(value => String(value)).filter(value => value.length > 0);
|
|
3604
|
+
const unique = [...new Set(terms)];
|
|
3605
|
+
if (unique.length < 1 || unique.length > 20)
|
|
3606
|
+
throw new Error("Provide term or between 1 and 20 terms");
|
|
3607
|
+
return unique;
|
|
3608
|
+
}
|
|
3609
|
+
function rankMemberCandidates(candidates, preferredFiles) {
|
|
3610
|
+
if (!preferredFiles?.length)
|
|
3042
3611
|
return;
|
|
3043
|
-
const
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
return
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
}
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3612
|
+
const ranks = new Map(preferredFiles.map((value, index) => [String(value).toUpperCase(), index]));
|
|
3613
|
+
candidates.sort((a, b) => (ranks.get(String(a.file).toUpperCase()) ?? Number.MAX_SAFE_INTEGER) -
|
|
3614
|
+
(ranks.get(String(b.file).toUpperCase()) ?? Number.MAX_SAFE_INTEGER) || String(a.file).localeCompare(String(b.file)));
|
|
3615
|
+
}
|
|
3616
|
+
function inspectMemberOperation(snapshot, operation, limits) {
|
|
3617
|
+
switch (operation.operation) {
|
|
3618
|
+
case "metadata":
|
|
3619
|
+
return { operation: "metadata", metadata: snapshot.metadata };
|
|
3620
|
+
case "read_ranges":
|
|
3621
|
+
return { operation: "read_ranges", ...formatMemberRead(snapshot, { ranges: operation.ranges, before: operation.before, after: operation.after }, limits) };
|
|
3622
|
+
case "search":
|
|
3623
|
+
return { operation: "search", ...searchSnapshot(snapshot, operation) };
|
|
3624
|
+
case "outline":
|
|
3625
|
+
return { operation: "outline", language: sourceLanguageOf(snapshot), symbols: outlineSnapshot(snapshot) };
|
|
3626
|
+
case "dependencies":
|
|
3627
|
+
return { operation: "dependencies", language: sourceLanguageOf(snapshot), dependencies: dependenciesFromSnapshot(snapshot) };
|
|
3628
|
+
default:
|
|
3629
|
+
throw new Error(`Unsupported inspection operation: ${operation.operation}`);
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
function searchSnapshot(snapshot, operation) {
|
|
3633
|
+
const terms = normalizeSearchTerms({ term: operation.query, terms: operation.terms });
|
|
3634
|
+
const before = clampInteger(operation.before, 0, 50, 0);
|
|
3635
|
+
const after = clampInteger(operation.after, 0, 50, 0);
|
|
3636
|
+
const maxMatches = clampInteger(operation.maxMatches, 1, 1_000, 100);
|
|
3637
|
+
const lowered = terms.map(term => term.toLowerCase());
|
|
3638
|
+
const matches = [];
|
|
3639
|
+
for (let index = 0; index < snapshot.lines.length && matches.length < maxMatches; index++) {
|
|
3640
|
+
const line = snapshot.lines[index];
|
|
3641
|
+
const matchedTerms = terms.filter((_term, termIndex) => line.toLowerCase().includes(lowered[termIndex]));
|
|
3642
|
+
if (matchedTerms.length === 0)
|
|
3643
|
+
continue;
|
|
3644
|
+
const start = Math.max(0, index - before);
|
|
3645
|
+
const end = Math.min(snapshot.lines.length - 1, index + after);
|
|
3646
|
+
matches.push({
|
|
3647
|
+
line: index + 1,
|
|
3648
|
+
content: line,
|
|
3649
|
+
matchedTerms,
|
|
3650
|
+
context: snapshot.lines.slice(start, end + 1).map((content, contextIndex) => ({ number: start + contextIndex + 1, content }))
|
|
3651
|
+
});
|
|
3065
3652
|
}
|
|
3066
|
-
return
|
|
3653
|
+
return { terms, matches, returned: matches.length, truncated: matches.length >= maxMatches };
|
|
3654
|
+
}
|
|
3655
|
+
function outlineSnapshot(snapshot) {
|
|
3656
|
+
return inspectSource(snapshot.lines, snapshot.metadata?.extension).symbols;
|
|
3657
|
+
}
|
|
3658
|
+
function dependenciesFromSnapshot(snapshot) {
|
|
3659
|
+
return inspectSource(snapshot.lines, snapshot.metadata?.extension).dependencies;
|
|
3660
|
+
}
|
|
3661
|
+
function sourceLanguageOf(snapshot) {
|
|
3662
|
+
return detectSourceLanguage(snapshot.metadata?.extension, snapshot.lines);
|
|
3663
|
+
}
|
|
3664
|
+
async function readSystemFacts(conn) {
|
|
3665
|
+
if (conn.__mcpSystemFacts)
|
|
3666
|
+
return conn.__mcpSystemFacts;
|
|
3667
|
+
try {
|
|
3668
|
+
const rows = await conn.runSQL("select OS_VERSION, OS_RELEASE, HOST_NAME, current_user as EFFECTIVE_USER " +
|
|
3669
|
+
"from SYSIBMADM.ENV_SYS_INFO fetch first 1 row only");
|
|
3670
|
+
const row = rows[0];
|
|
3671
|
+
const version = String(row?.OS_VERSION || "").replace(/\D/g, "");
|
|
3672
|
+
const release = String(row?.OS_RELEASE || "").replace(/\D/g, "");
|
|
3673
|
+
const facts = {
|
|
3674
|
+
...(version && release ? { release: `${Number(version)}.${Number(release)}` } : {}),
|
|
3675
|
+
...(row?.EFFECTIVE_USER ? { effectiveUser: String(row.EFFECTIVE_USER).trim() } : {}),
|
|
3676
|
+
...(row?.HOST_NAME ? { systemName: String(row.HOST_NAME).trim() } : {})
|
|
3677
|
+
};
|
|
3678
|
+
conn.__mcpSystemFacts = facts;
|
|
3679
|
+
return facts;
|
|
3680
|
+
}
|
|
3681
|
+
catch {
|
|
3682
|
+
// Older IBM i releases may not expose SYSIBMADM.ENV_SYS_INFO.
|
|
3683
|
+
}
|
|
3684
|
+
return {};
|
|
3685
|
+
}
|
|
3686
|
+
export function clampInteger(value, minimum, maximum, fallback) {
|
|
3687
|
+
const parsed = Number(value);
|
|
3688
|
+
if (!Number.isFinite(parsed))
|
|
3689
|
+
return fallback;
|
|
3690
|
+
return Math.max(minimum, Math.min(maximum, Math.floor(parsed)));
|
|
3067
3691
|
}
|
|
3068
3692
|
function rejectCredentialArgs(args, toolName) {
|
|
3069
3693
|
if (!args || typeof args !== "object")
|
|
@@ -3075,19 +3699,19 @@ function rejectCredentialArgs(args, toolName) {
|
|
|
3075
3699
|
throw new Error(`${toolName} does not accept storePassword. Credentials must be managed via local control-plane UI.`);
|
|
3076
3700
|
}
|
|
3077
3701
|
}
|
|
3078
|
-
function result(text) {
|
|
3702
|
+
export function result(text) {
|
|
3079
3703
|
return {
|
|
3080
3704
|
content: [{ type: "text", text }],
|
|
3081
3705
|
structuredContent: { ok: true, text }
|
|
3082
3706
|
};
|
|
3083
3707
|
}
|
|
3084
|
-
function json(obj) {
|
|
3708
|
+
export function json(obj) {
|
|
3085
3709
|
return {
|
|
3086
3710
|
content: [{ type: "text", text: JSON.stringify(obj, null, 2) }],
|
|
3087
3711
|
structuredContent: { ok: true, data: obj }
|
|
3088
3712
|
};
|
|
3089
3713
|
}
|
|
3090
|
-
function toolError(message, error) {
|
|
3714
|
+
export function toolError(message, error) {
|
|
3091
3715
|
return {
|
|
3092
3716
|
isError: true,
|
|
3093
3717
|
content: [{ type: "text", text: message }],
|
|
@@ -3100,13 +3724,179 @@ function toolError(message, error) {
|
|
|
3100
3724
|
}
|
|
3101
3725
|
};
|
|
3102
3726
|
}
|
|
3103
|
-
function
|
|
3727
|
+
export function jsonError(message, obj) {
|
|
3728
|
+
return {
|
|
3729
|
+
isError: true,
|
|
3730
|
+
content: [{ type: "text", text: `${message}\n${JSON.stringify(obj, null, 2)}` }],
|
|
3731
|
+
structuredContent: {
|
|
3732
|
+
ok: false,
|
|
3733
|
+
data: obj,
|
|
3734
|
+
error: { message }
|
|
3735
|
+
}
|
|
3736
|
+
};
|
|
3737
|
+
}
|
|
3738
|
+
// Returns the payload as normal JSON when the command worked and as a tool
|
|
3739
|
+
// error (isError) when it did not, so agents can't mistake a failure for "OK".
|
|
3740
|
+
export function commandOutcome(payload) {
|
|
3741
|
+
if (payload.ok)
|
|
3742
|
+
return json(payload);
|
|
3743
|
+
const detail = String(payload.stderr || "").trim().split(/\r?\n/)[0] || "";
|
|
3744
|
+
const label = payload.command ? `Command failed: ${payload.command}` : "Command failed";
|
|
3745
|
+
return jsonError(detail ? `${label} (${detail})` : label, payload);
|
|
3746
|
+
}
|
|
3747
|
+
export function requireCommandSuccess(res, label) {
|
|
3748
|
+
if (!res || res.code !== 0) {
|
|
3749
|
+
const detail = String(res?.stderr || res?.stdout || "").trim();
|
|
3750
|
+
throw new Error(`${label} failed (exit ${res?.code ?? "unknown"})${detail ? `: ${detail}` : ""}`);
|
|
3751
|
+
}
|
|
3752
|
+
return res;
|
|
3753
|
+
}
|
|
3754
|
+
const IFS_READ_DEFAULT_MAX_BYTES = 1024 * 1024;
|
|
3755
|
+
const IFS_READ_HARD_MAX_BYTES = 16 * 1024 * 1024;
|
|
3756
|
+
// Size of an IFS stream file from `ls -ldL`, or undefined when it can't be
|
|
3757
|
+
// determined (the download is then checked after the fact).
|
|
3758
|
+
export async function statIfsFileSize(conn, remotePath) {
|
|
3759
|
+
try {
|
|
3760
|
+
const res = await conn.sendCommand({ command: `ls -ldL ${Tools.escapePath(remotePath)}` });
|
|
3761
|
+
if (!res || res.code !== 0)
|
|
3762
|
+
return undefined;
|
|
3763
|
+
const parts = String(res.stdout || "").trim().split(/\s+/);
|
|
3764
|
+
if (parts.length < 5 || parts[0].startsWith("d"))
|
|
3765
|
+
return undefined;
|
|
3766
|
+
const size = Number(parts[4]);
|
|
3767
|
+
return Number.isFinite(size) && size >= 0 ? size : undefined;
|
|
3768
|
+
}
|
|
3769
|
+
catch {
|
|
3770
|
+
return undefined;
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
// Decodes data as UTF-8 text, or returns undefined when it is binary (contains
|
|
3774
|
+
// NUL bytes or is not valid UTF-8).
|
|
3775
|
+
export function decodeUtf8Text(data) {
|
|
3776
|
+
if (data.includes(0))
|
|
3777
|
+
return undefined;
|
|
3778
|
+
try {
|
|
3779
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(data);
|
|
3780
|
+
}
|
|
3781
|
+
catch {
|
|
3782
|
+
return undefined;
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
const SQL_TRAILING_ROW_CLAUSE = new RegExp("(?:fetch\\s+(?:first|next)\\s+(?:\\S+\\s+)?rows?\\s+only" +
|
|
3786
|
+
"|for\\s+(?:read|fetch)\\s+only" +
|
|
3787
|
+
"|for\\s+update(?:\\s+of\\s+[\\w\\s,\"]+)?" +
|
|
3788
|
+
"|optimize\\s+for\\s+\\S+\\s+rows?" +
|
|
3789
|
+
"|with\\s+(?:nc|ur|cs|rs|rr)(?:\\s+keep\\s+(?:update|exclusive|share)\\s+locks)?" +
|
|
3790
|
+
"|limit\\s+\\S+(?:\\s+offset\\s+\\S+)?" +
|
|
3791
|
+
"|offset\\s+\\S+\\s+rows?" +
|
|
3792
|
+
"|skip\\s+locked\\s+data|use\\s+currently\\s+committed|wait\\s+for\\s+outcome)$");
|
|
3793
|
+
// Adds FETCH FIRST n ROWS ONLY to a SELECT/WITH query so the row cap is applied
|
|
3794
|
+
// on the IBM i. Queries that already end with a row-limiting, locking or
|
|
3795
|
+
// isolation clause are left unchanged (the client-side slice still applies).
|
|
3796
|
+
function applySqlRowCap(sql, rowLimit) {
|
|
3797
|
+
const body = sql.replace(/[\s;]+$/, "");
|
|
3798
|
+
const normalized = body
|
|
3799
|
+
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
3800
|
+
.split("\n")
|
|
3801
|
+
.map(line => line.replace(/--.*$/, ""))
|
|
3802
|
+
.join(" ")
|
|
3803
|
+
.replace(/\s+/g, " ")
|
|
3804
|
+
.trim()
|
|
3805
|
+
.toLowerCase();
|
|
3806
|
+
if (!/^(?:select|with)\b/.test(normalized))
|
|
3807
|
+
return { sql, applied: false };
|
|
3808
|
+
if (SQL_TRAILING_ROW_CLAUSE.test(normalized))
|
|
3809
|
+
return { sql, applied: false };
|
|
3810
|
+
return { sql: `${body}\nfetch first ${Math.max(1, Math.floor(rowLimit))} rows only`, applied: true };
|
|
3811
|
+
}
|
|
3812
|
+
function requireSpoolJobFilter(value) {
|
|
3813
|
+
const raw = String(value || "").trim().toUpperCase();
|
|
3814
|
+
return raw.includes("/") ? requireJobName(raw) : requireIbmObjectName(raw, "jobName");
|
|
3815
|
+
}
|
|
3816
|
+
export function requireObjectTypeValue(value, label) {
|
|
3817
|
+
const objectType = String(value || "").trim().toUpperCase();
|
|
3818
|
+
if (!/^\*[A-Z]{2,9}$/.test(objectType))
|
|
3819
|
+
throw new Error(`Invalid ${label}: ${value}`);
|
|
3820
|
+
return objectType;
|
|
3821
|
+
}
|
|
3822
|
+
// SNDMSG only sends *INFO and *INQ messages; *COMP and *DIAG need SNDPGMMSG.
|
|
3823
|
+
function buildSendMessageCommand(message, library, messageQueue, messageType) {
|
|
3824
|
+
const queue = `${library}/${messageQueue}`;
|
|
3825
|
+
switch (messageType) {
|
|
3826
|
+
case "*INQ":
|
|
3827
|
+
return `SNDMSG MSG(${quoteClString(message)}) TOMSGQ(${queue}) MSGTYPE(*INQ) RPYMSGQ(${queue})`;
|
|
3828
|
+
case "*COMP":
|
|
3829
|
+
case "*DIAG":
|
|
3830
|
+
return `SNDPGMMSG MSG(${quoteClString(message)}) TOMSGQ(${queue}) MSGTYPE(${messageType})`;
|
|
3831
|
+
default:
|
|
3832
|
+
return `SNDMSG MSG(${quoteClString(message)}) TOMSGQ(${queue}) MSGTYPE(*INFO)`;
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
// CALL to the Send Data Queue (QSNDDTAQ) API with explicitly typed parameters:
|
|
3836
|
+
// queue name CHAR(10), library CHAR(10), data length DEC(5,0), data.
|
|
3837
|
+
function buildQsnddtaqCall(library, queue, message) {
|
|
3838
|
+
if (/[^\x20-\x7E]/.test(message)) {
|
|
3839
|
+
throw new Error("QSYS2.SEND_DATA_QUEUE is not available and the QSNDDTAQ fallback only supports printable single-byte text.");
|
|
3840
|
+
}
|
|
3841
|
+
if (message.length > 5000) {
|
|
3842
|
+
throw new Error("QSYS2.SEND_DATA_QUEUE is not available and the QSNDDTAQ fallback is limited to 5000 characters.");
|
|
3843
|
+
}
|
|
3844
|
+
const length = message.length;
|
|
3845
|
+
return `CALL PGM(QSYS/QSNDDTAQ) PARM((${quoteClString(queue)} (*CHAR 10)) (${quoteClString(library)} (*CHAR 10)) ` +
|
|
3846
|
+
`(${length} (*DEC 5 0)) (${quoteClString(message)} (*CHAR ${length})))`;
|
|
3847
|
+
}
|
|
3848
|
+
// Formats CHGDTAARA for the data area's type: *DEC takes an unquoted number and
|
|
3849
|
+
// *LGL takes '0' or '1' (neither allows a substring); *CHAR keeps the
|
|
3850
|
+
// substring form so only the given positions change.
|
|
3851
|
+
function buildChangeDataAreaCommand(library, dataArea, value, info, args) {
|
|
3852
|
+
const target = `${library}/${dataArea}`;
|
|
3853
|
+
const type = String(info?.type || "*CHAR").trim().toUpperCase();
|
|
3854
|
+
const substringRequested = args?.startPosition !== undefined || args?.length !== undefined;
|
|
3855
|
+
if (type === "*DEC") {
|
|
3856
|
+
if (substringRequested)
|
|
3857
|
+
throw new Error("startPosition and length are only valid for *CHAR data areas");
|
|
3858
|
+
const text = value.trim();
|
|
3859
|
+
const match = text.match(/^([+-]?)(\d*)(?:\.(\d*))?$/);
|
|
3860
|
+
if (!match || (match[2] === "" && !match[3]))
|
|
3861
|
+
throw new Error(`Value for *DEC data area must be numeric: ${value}`);
|
|
3862
|
+
const intDigits = match[2].replace(/^0+(?=\d)/, "");
|
|
3863
|
+
const fraction = match[3] || "";
|
|
3864
|
+
const decimals = typeof info?.decimalPositions === "number" ? info.decimalPositions : undefined;
|
|
3865
|
+
const totalDigits = typeof info?.length === "number" ? info.length : undefined;
|
|
3866
|
+
if (decimals !== undefined && fraction.replace(/0+$/, "").length > decimals) {
|
|
3867
|
+
throw new Error(`Value has more than ${decimals} decimal positions`);
|
|
3868
|
+
}
|
|
3869
|
+
if (totalDigits !== undefined && decimals !== undefined && intDigits.replace(/^0+/, "").length > totalDigits - decimals) {
|
|
3870
|
+
throw new Error(`Value does not fit a *DEC ${totalDigits} ${decimals} data area`);
|
|
3871
|
+
}
|
|
3872
|
+
const formatted = `${match[1] === "-" ? "-" : ""}${intDigits || "0"}${fraction ? `.${fraction}` : ""}`;
|
|
3873
|
+
return `CHGDTAARA DTAARA(${target}) VALUE(${formatted})`;
|
|
3874
|
+
}
|
|
3875
|
+
if (type === "*LGL") {
|
|
3876
|
+
if (substringRequested)
|
|
3877
|
+
throw new Error("startPosition and length are only valid for *CHAR data areas");
|
|
3878
|
+
const normalized = value.trim().toLowerCase();
|
|
3879
|
+
const logical = ["1", "true", "*on", "on", "yes"].includes(normalized)
|
|
3880
|
+
? "1"
|
|
3881
|
+
: ["0", "false", "*off", "off", "no"].includes(normalized) ? "0" : undefined;
|
|
3882
|
+
if (!logical)
|
|
3883
|
+
throw new Error(`Value for *LGL data area must be 0 or 1: ${value}`);
|
|
3884
|
+
return `CHGDTAARA DTAARA(${target}) VALUE('${logical}')`;
|
|
3885
|
+
}
|
|
3886
|
+
const startPosition = clampNumber(args?.startPosition, 1, 1, 2000);
|
|
3887
|
+
const length = clampNumber(args?.length, Math.max(value.length, 1), 1, 2000);
|
|
3888
|
+
if (typeof info?.length === "number" && info.length > 0 && startPosition + length - 1 > info.length) {
|
|
3889
|
+
throw new Error(`Positions ${startPosition} to ${startPosition + length - 1} are outside the ${info.length}-character data area`);
|
|
3890
|
+
}
|
|
3891
|
+
return `CHGDTAARA DTAARA(${target} (${startPosition} ${length})) VALUE(${quoteClString(value)})`;
|
|
3892
|
+
}
|
|
3893
|
+
export function getConnectionName(ctx, args) {
|
|
3104
3894
|
const name = args?.connectionName || ctx.activeName;
|
|
3105
3895
|
if (!name)
|
|
3106
3896
|
throw new Error("connectionName is required (no active connection)");
|
|
3107
3897
|
return name;
|
|
3108
3898
|
}
|
|
3109
|
-
function requireQsysName(conn, value, label) {
|
|
3899
|
+
export function requireQsysName(conn, value, label) {
|
|
3110
3900
|
const raw = String(value || "").trim().toUpperCase();
|
|
3111
3901
|
if (!raw)
|
|
3112
3902
|
throw new Error(`${label} is required`);
|
|
@@ -3151,7 +3941,7 @@ function requireMessageKey(value) {
|
|
|
3151
3941
|
}
|
|
3152
3942
|
throw new Error(`Invalid messageKey: ${value}`);
|
|
3153
3943
|
}
|
|
3154
|
-
async function updateConnectionSettings(ctx, name, update) {
|
|
3944
|
+
export async function updateConnectionSettings(ctx, name, update) {
|
|
3155
3945
|
const conn = await ctx.store.getConnection(name);
|
|
3156
3946
|
if (!conn)
|
|
3157
3947
|
throw new Error(`Connection ${name} not found`);
|
|
@@ -3165,21 +3955,52 @@ async function updateConnectionSettings(ctx, name, update) {
|
|
|
3165
3955
|
// No active session for this connection.
|
|
3166
3956
|
}
|
|
3167
3957
|
}
|
|
3168
|
-
|
|
3958
|
+
/**
|
|
3959
|
+
* applyToJob: puts the library list in effect now. A connected client applies
|
|
3960
|
+
* its configured list (which the caller has just updated) to the persistent
|
|
3961
|
+
* SQL job, or checks it for the per-call QZDFMDB2 and qsh jobs; libraries that
|
|
3962
|
+
* do not exist are left out and reported in `warning` instead of failing.
|
|
3963
|
+
*/
|
|
3964
|
+
export async function applyLibraryList(conn, libraryList, currentLibrary) {
|
|
3965
|
+
const names = [...(libraryList || []), ...(currentLibrary ? [currentLibrary] : [])].map(l => String(l).toUpperCase());
|
|
3966
|
+
const invalid = names.find(l => !conn.validQsysName(l));
|
|
3967
|
+
if (invalid)
|
|
3968
|
+
throw new Error(`Invalid library name: ${invalid}`);
|
|
3969
|
+
if (typeof conn.applyConfiguredLibraryList === "function") {
|
|
3970
|
+
const applied = await conn.applyConfiguredLibraryList();
|
|
3971
|
+
return applied?.warning ? { warning: applied.warning } : {};
|
|
3972
|
+
}
|
|
3169
3973
|
if (libraryList && libraryList.length > 0) {
|
|
3170
3974
|
const normalized = libraryList.map((l) => String(l).toUpperCase());
|
|
3171
3975
|
const invalid = normalized.find((l) => !conn.validQsysName(l));
|
|
3172
3976
|
if (invalid)
|
|
3173
3977
|
throw new Error(`Invalid library name: ${invalid}`);
|
|
3174
|
-
|
|
3175
|
-
|
|
3978
|
+
if (typeof conn.runCL === "function") {
|
|
3979
|
+
// Runs in the persistent SQL job when there is one, so later SQL and CL see it.
|
|
3980
|
+
requireCommandSuccess(await conn.runCL(`CHGLIBL LIBL(${normalized.join(" ")})`), "CHGLIBL");
|
|
3981
|
+
}
|
|
3982
|
+
else {
|
|
3983
|
+
// '$' is escaped so names like $LIB are not expanded by qsh.
|
|
3984
|
+
const libs = normalized.join(" ").replace(/\$/g, "\\$");
|
|
3985
|
+
requireCommandSuccess(await conn.sendQsh({ command: `system \"CHGLIBL LIBL(${libs})\"` }), "CHGLIBL");
|
|
3986
|
+
}
|
|
3176
3987
|
}
|
|
3177
3988
|
if (currentLibrary) {
|
|
3178
3989
|
const lib = String(currentLibrary).toUpperCase();
|
|
3179
3990
|
if (!conn.validQsysName(lib))
|
|
3180
3991
|
throw new Error(`Invalid current library: ${lib}`);
|
|
3181
|
-
|
|
3992
|
+
if (typeof conn.runCL === "function") {
|
|
3993
|
+
requireCommandSuccess(await conn.runCL(`CHGCURLIB CURLIB(${lib})`), "CHGCURLIB");
|
|
3994
|
+
}
|
|
3995
|
+
else {
|
|
3996
|
+
requireCommandSuccess(await conn.sendQsh({ command: `system \"CHGCURLIB ${lib.replace(/\$/g, "\\$")}\"` }), "CHGCURLIB");
|
|
3997
|
+
}
|
|
3182
3998
|
}
|
|
3999
|
+
return {};
|
|
4000
|
+
}
|
|
4001
|
+
/** "OK", plus the library list warning from applyToJob when there is one. */
|
|
4002
|
+
function okApplied(applied, text = "OK") {
|
|
4003
|
+
return result(applied?.warning ? `${text}. Warning: ${applied.warning}` : text);
|
|
3183
4004
|
}
|
|
3184
4005
|
function stripUndefined(obj) {
|
|
3185
4006
|
const out = {};
|
|
@@ -3247,6 +4068,42 @@ async function compareDeployTrees(conn, localRoot, remoteRoot) {
|
|
|
3247
4068
|
}
|
|
3248
4069
|
};
|
|
3249
4070
|
}
|
|
4071
|
+
/**
|
|
4072
|
+
* Re-checks every file present on both sides by SHA-256, so files with equal
|
|
4073
|
+
* size but different content count as changed and size-unknown files are
|
|
4074
|
+
* resolved. Files whose remote hash can't be computed stay unresolved.
|
|
4075
|
+
*/
|
|
4076
|
+
async function refineComparisonByHash(conn, comparison) {
|
|
4077
|
+
const sizeOf = new Map();
|
|
4078
|
+
for (const entry of [...comparison.changed, ...comparison.unresolved])
|
|
4079
|
+
sizeOf.set(entry.path, entry);
|
|
4080
|
+
const shared = [...comparison.identical, ...comparison.changed.map(e => e.path), ...comparison.unresolved.map(e => e.path)]
|
|
4081
|
+
.sort((a, b) => a.localeCompare(b));
|
|
4082
|
+
const remoteHashes = await sha256RemoteFiles(conn, comparison.remoteRoot, shared);
|
|
4083
|
+
const identical = [];
|
|
4084
|
+
const changed = [];
|
|
4085
|
+
const unresolved = [];
|
|
4086
|
+
for (const rel of shared) {
|
|
4087
|
+
const sizes = sizeOf.get(rel);
|
|
4088
|
+
const localSha256 = await sha256LocalFile(path.join(comparison.localRoot, rel));
|
|
4089
|
+
const remoteSha256 = remoteHashes.get(rel);
|
|
4090
|
+
const base = { path: rel, localSize: sizes?.localSize, remoteSize: sizes?.remoteSize, localSha256, remoteSha256 };
|
|
4091
|
+
if (!remoteSha256)
|
|
4092
|
+
unresolved.push({ ...base, reason: "hash_unavailable" });
|
|
4093
|
+
else if (remoteSha256 === localSha256)
|
|
4094
|
+
identical.push(rel);
|
|
4095
|
+
else
|
|
4096
|
+
changed.push({ ...base, reason: "content_mismatch" });
|
|
4097
|
+
}
|
|
4098
|
+
return {
|
|
4099
|
+
...comparison,
|
|
4100
|
+
comparedBy: "sha256",
|
|
4101
|
+
identical,
|
|
4102
|
+
changed,
|
|
4103
|
+
unresolved,
|
|
4104
|
+
summary: { ...comparison.summary, identical: identical.length, changed: changed.length, unresolved: unresolved.length }
|
|
4105
|
+
};
|
|
4106
|
+
}
|
|
3250
4107
|
async function listLocalFiles(root) {
|
|
3251
4108
|
const resolvedRoot = path.resolve(root);
|
|
3252
4109
|
const results = new Map();
|
|
@@ -3271,6 +4128,10 @@ async function listRemoteFiles(conn, remoteRoot) {
|
|
|
3271
4128
|
const normalizedRoot = remoteRoot.replace(/\/$/, "");
|
|
3272
4129
|
const find = conn.remoteFeatures.find || "/QOpenSys/pkgs/bin/find";
|
|
3273
4130
|
const stat = conn.remoteFeatures.stat;
|
|
4131
|
+
// A remote root that does not exist yet is an empty tree (first deploy).
|
|
4132
|
+
const exists = await conn.sendCommand({ command: `test -d ${Tools.escapePath(normalizedRoot)}` });
|
|
4133
|
+
if (exists.code !== 0)
|
|
4134
|
+
return new Map();
|
|
3274
4135
|
if (stat) {
|
|
3275
4136
|
const statCmd = `${find} ${Tools.escapePath(normalizedRoot)} -type f -exec ${stat} -c '%n|%s' {} \\;`;
|
|
3276
4137
|
const statRes = await conn.sendCommand({ command: statCmd });
|
|
@@ -3321,7 +4182,7 @@ function joinRemotePath(root, rel) {
|
|
|
3321
4182
|
const base = root.replace(/\/$/, "");
|
|
3322
4183
|
return `${base}/${rel}`.replace(/\\/g, "/");
|
|
3323
4184
|
}
|
|
3324
|
-
function mapActionExecution(action, targetType, targetPath, result) {
|
|
4185
|
+
export function mapActionExecution(action, targetType, targetPath, result) {
|
|
3325
4186
|
const diagnostics = normalizeCommandDiagnostics(result);
|
|
3326
4187
|
const code = Number(result?.code);
|
|
3327
4188
|
const ok = Number.isFinite(code) ? code === 0 : false;
|
|
@@ -3385,32 +4246,60 @@ function normalizeJoblogRow(row) {
|
|
|
3385
4246
|
timestamp: row.MESSAGE_TIMESTAMP ? String(row.MESSAGE_TIMESTAMP) : undefined
|
|
3386
4247
|
};
|
|
3387
4248
|
}
|
|
3388
|
-
function normalizeSpoolEntry(row) {
|
|
4249
|
+
export function normalizeSpoolEntry(row) {
|
|
3389
4250
|
return {
|
|
3390
4251
|
...row,
|
|
3391
4252
|
source: "spool",
|
|
3392
|
-
jobName: toNullableString(pickFirst(row, ["JOB_NAME", "JOB"])) || "",
|
|
4253
|
+
jobName: toNullableString(pickFirst(row, ["QUALIFIED_JOB_NAME", "JOB_NAME", "JOB"])) || "",
|
|
3393
4254
|
spooledFileName: toNullableString(pickFirst(row, ["SPOOLED_FILE_NAME", "SPOOLED_FILE", "SPLF_NAME"])) || "",
|
|
3394
4255
|
spooledFileNumber: toNullableNumber(pickFirst(row, ["SPOOLED_FILE_NUMBER", "SPLF_NUMBER", "FILE_NUMBER"])),
|
|
3395
4256
|
outputQueue: toNullableString(pickFirst(row, ["OUTPUT_QUEUE_NAME", "OUTQ_NAME", "OUTPUT_QUEUE"])) || "",
|
|
4257
|
+
outputQueueLibrary: toNullableString(pickFirst(row, ["OUTPUT_QUEUE_LIBRARY_NAME", "OUTPUT_QUEUE_LIBRARY"])) || "",
|
|
4258
|
+
userName: toNullableString(pickFirst(row, ["USER_NAME", "JOB_USER"])) || "",
|
|
4259
|
+
createdAt: toNullableString(pickFirst(row, ["CREATE_TIMESTAMP", "CREATION_TIMESTAMP"])),
|
|
3396
4260
|
totalPages: toNullableNumber(pickFirst(row, ["TOTAL_PAGES", "PAGES"])),
|
|
3397
4261
|
status: toNullableString(pickFirst(row, ["FILE_STATUS", "STATUS"])) || ""
|
|
3398
4262
|
};
|
|
3399
4263
|
}
|
|
3400
|
-
function normalizeSpoolLine(row) {
|
|
4264
|
+
export function normalizeSpoolLine(row) {
|
|
4265
|
+
const position = pickFirst(row, ["ORDINAL_POSITION", "LINE_NUMBER"]);
|
|
3401
4266
|
return {
|
|
3402
4267
|
...row,
|
|
3403
4268
|
source: "spool",
|
|
3404
|
-
lineNumber: Number.isFinite(Number(
|
|
3405
|
-
text: String(row.SPOOLED_DATA
|
|
4269
|
+
lineNumber: Number.isFinite(Number(position)) && position !== null && position !== "" ? Number(position) : null,
|
|
4270
|
+
text: String(row.SPOOLED_DATA ?? "")
|
|
3406
4271
|
};
|
|
3407
4272
|
}
|
|
3408
|
-
function clampNumber(value, fallback, min, max) {
|
|
4273
|
+
export function clampNumber(value, fallback, min, max) {
|
|
3409
4274
|
const n = Number(value);
|
|
3410
4275
|
if (!Number.isFinite(n))
|
|
3411
4276
|
return fallback;
|
|
3412
4277
|
return Math.max(min, Math.min(max, Math.floor(n)));
|
|
3413
4278
|
}
|
|
4279
|
+
/**
|
|
4280
|
+
* Runs SQL with a time limit. When the limit is reached the call fails and the
|
|
4281
|
+
* signal asks the client to cancel the statement in its persistent SQL job, so
|
|
4282
|
+
* it does not keep running on the IBM i.
|
|
4283
|
+
*/
|
|
4284
|
+
async function runSqlWithTimeout(conn, sql, timeoutMs, timeoutMessage) {
|
|
4285
|
+
const controller = new AbortController();
|
|
4286
|
+
let timeoutHandle;
|
|
4287
|
+
try {
|
|
4288
|
+
return await Promise.race([
|
|
4289
|
+
conn.runSQL(sql, { signal: controller.signal }),
|
|
4290
|
+
new Promise((_, reject) => {
|
|
4291
|
+
timeoutHandle = setTimeout(() => {
|
|
4292
|
+
controller.abort();
|
|
4293
|
+
reject(new Error(timeoutMessage));
|
|
4294
|
+
}, timeoutMs);
|
|
4295
|
+
})
|
|
4296
|
+
]);
|
|
4297
|
+
}
|
|
4298
|
+
finally {
|
|
4299
|
+
if (timeoutHandle)
|
|
4300
|
+
clearTimeout(timeoutHandle);
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
3414
4303
|
async function withTimeout(promise, timeoutMs, timeoutMessage) {
|
|
3415
4304
|
let timeoutHandle;
|
|
3416
4305
|
try {
|
|
@@ -3427,14 +4316,7 @@ async function withTimeout(promise, timeoutMs, timeoutMessage) {
|
|
|
3427
4316
|
}
|
|
3428
4317
|
}
|
|
3429
4318
|
function getSqlStatementType(sql) {
|
|
3430
|
-
|
|
3431
|
-
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
3432
|
-
.split("\n")
|
|
3433
|
-
.filter(line => !line.trim().startsWith("--"))
|
|
3434
|
-
.join(" ")
|
|
3435
|
-
.trim();
|
|
3436
|
-
const token = stripped.split(/\s+/)[0] || "UNKNOWN";
|
|
3437
|
-
return token.toUpperCase();
|
|
4319
|
+
return parseSqlStatementType(sql);
|
|
3438
4320
|
}
|
|
3439
4321
|
function getRowColumns(rows) {
|
|
3440
4322
|
if (!Array.isArray(rows) || rows.length === 0)
|
|
@@ -3442,6 +4324,10 @@ function getRowColumns(rows) {
|
|
|
3442
4324
|
return Object.keys(rows[0] || {});
|
|
3443
4325
|
}
|
|
3444
4326
|
function inferAffectedRows(rows) {
|
|
4327
|
+
// The persistent SQL job reports the update count of INSERT/UPDATE/DELETE.
|
|
4328
|
+
const updateCount = rows?.updateCount;
|
|
4329
|
+
if (typeof updateCount === "number" && Number.isFinite(updateCount))
|
|
4330
|
+
return updateCount;
|
|
3445
4331
|
if (!Array.isArray(rows) || rows.length === 0)
|
|
3446
4332
|
return null;
|
|
3447
4333
|
const candidateKeys = [
|
|
@@ -3486,37 +4372,111 @@ function toSafeSqlError(err, sql, timeoutMs) {
|
|
|
3486
4372
|
};
|
|
3487
4373
|
}
|
|
3488
4374
|
function isReadOnlySql(sql) {
|
|
3489
|
-
|
|
3490
|
-
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
3491
|
-
.split("\n")
|
|
3492
|
-
.filter(line => !line.trim().startsWith("--"))
|
|
3493
|
-
.join(" ")
|
|
3494
|
-
.trim()
|
|
3495
|
-
.toLowerCase();
|
|
3496
|
-
return stripped.startsWith("select ") || stripped.startsWith("with ") || stripped.startsWith("values ");
|
|
4375
|
+
return checkReadOnlySql(sql).readOnly;
|
|
3497
4376
|
}
|
|
3498
|
-
function enforceWritable(ctx, conn, operation, args) {
|
|
4377
|
+
export async function enforceWritable(ctx, conn, operation, args) {
|
|
3499
4378
|
if (conn.getConfig().readOnlyMode)
|
|
3500
4379
|
throw new Error("Connection is in read-only mode");
|
|
3501
|
-
enforcePolicyOperation(ctx, operation, args);
|
|
4380
|
+
await enforcePolicyOperation(ctx, operation, args);
|
|
3502
4381
|
}
|
|
3503
|
-
function enforcePolicyOperation(ctx, operation, args) {
|
|
4382
|
+
async function enforcePolicyOperation(ctx, operation, args) {
|
|
3504
4383
|
const policy = ctx.getPolicy(args?.connectionName);
|
|
3505
4384
|
if (policy.profile === "read-only") {
|
|
3506
4385
|
throw new Error(`Blocked by policy (${policy.profile}): ${operation}`);
|
|
3507
4386
|
}
|
|
3508
|
-
|
|
3509
|
-
|
|
4387
|
+
const deniedReason = isDeniedByPolicy(policy, operation, args);
|
|
4388
|
+
if (deniedReason) {
|
|
4389
|
+
throw new Error(`Blocked by policy: ${operation} (${deniedReason})`);
|
|
4390
|
+
}
|
|
4391
|
+
if (policy.profile !== "guarded")
|
|
4392
|
+
return;
|
|
4393
|
+
// Shell text can do anything the profile can, so under guarded it always
|
|
4394
|
+
// needs the person's say, whatever requireApprovalFor lists.
|
|
4395
|
+
if (isShellOperation(args)) {
|
|
4396
|
+
await requireConfirmation(ctx, policy, "shell.run", args, false);
|
|
4397
|
+
}
|
|
4398
|
+
else if (requiresApproval(policy, operation)) {
|
|
4399
|
+
await requireConfirmation(ctx, policy, operation, args, operation !== "ifs.delete");
|
|
3510
4400
|
}
|
|
3511
|
-
|
|
3512
|
-
|
|
4401
|
+
}
|
|
4402
|
+
/**
|
|
4403
|
+
* Get the person's approval for a guarded operation. With approvalMode
|
|
4404
|
+
* "prompt" (the default) the MCP client asks them; the agent's approve flag
|
|
4405
|
+
* counts for nothing. With "agent", approve=true is enough.
|
|
4406
|
+
*/
|
|
4407
|
+
export async function requireConfirmation(ctx, policy, operation, args, allowSession) {
|
|
4408
|
+
if (policy.approvalMode === "agent") {
|
|
4409
|
+
if (!Boolean(args?.approve)) {
|
|
4410
|
+
throw new Error(`Operation '${operation}' requires approve=true under '${policy.profile}' policy.`);
|
|
4411
|
+
}
|
|
4412
|
+
return;
|
|
4413
|
+
}
|
|
4414
|
+
const connectionName = String(args?.connectionName || ctx.activeName || "");
|
|
4415
|
+
const sessionKey = `${connectionName}\u0000${operation}`;
|
|
4416
|
+
if (allowSession && ctx.sessionApprovals?.has(sessionKey))
|
|
4417
|
+
return;
|
|
4418
|
+
const unsupported = new Error(`Operation '${operation}' needs the user's confirmation, but this MCP client cannot show confirmation prompts. ` +
|
|
4419
|
+
"Ask the user to run it from a client that supports MCP elicitation, or to set this connection's approval mode " +
|
|
4420
|
+
"to \"agent\" in config.json (approve=true is then accepted).");
|
|
4421
|
+
if (!ctx.confirm)
|
|
4422
|
+
throw unsupported;
|
|
4423
|
+
const answer = await ctx.confirm({
|
|
4424
|
+
operation,
|
|
4425
|
+
connectionName,
|
|
4426
|
+
toolName: toolCallStore.getStore()?.toolName,
|
|
4427
|
+
details: describeOperation(args),
|
|
4428
|
+
allowSession
|
|
4429
|
+
});
|
|
4430
|
+
if (answer === "unsupported")
|
|
4431
|
+
throw unsupported;
|
|
4432
|
+
if (!answer.allowed) {
|
|
4433
|
+
throw new Error(`Operation '${operation}' was not confirmed by the user, so it did not run.`);
|
|
4434
|
+
}
|
|
4435
|
+
if (allowSession && answer.forSession)
|
|
4436
|
+
ctx.sessionApprovals?.add(sessionKey);
|
|
4437
|
+
}
|
|
4438
|
+
/**
|
|
4439
|
+
* Approval for operations that always need it, whatever requireApprovalFor
|
|
4440
|
+
* lists: under guarded the person is asked (no session allowance); other
|
|
4441
|
+
* profiles keep the agent's approve=true.
|
|
4442
|
+
*/
|
|
4443
|
+
export async function requireDangerousApproval(ctx, operation, args, approveMessage) {
|
|
4444
|
+
const policy = ctx.getPolicy(args?.connectionName);
|
|
4445
|
+
if (policy.profile === "guarded") {
|
|
4446
|
+
await requireConfirmation(ctx, policy, operation, args, false);
|
|
4447
|
+
return;
|
|
4448
|
+
}
|
|
4449
|
+
if (!Boolean(args?.approve))
|
|
4450
|
+
throw new Error(approveMessage);
|
|
4451
|
+
}
|
|
4452
|
+
const DESCRIBED_ARG_KEYS = [
|
|
4453
|
+
"command", "environment", "cwd", "sql",
|
|
4454
|
+
"library", "file", "sourceFile", "member", "object", "type", "objectType", "name", "newName",
|
|
4455
|
+
"path", "remotePath", "remoteDirectory", "targetPath", "localPath", "recursive", "confirmPath",
|
|
4456
|
+
"deleteExtraRemote", "libraryList", "currentLibrary", "remove",
|
|
4457
|
+
"confirmName", "journalLibrary", "journal", "retentionDays", "maxDeletes", "before"
|
|
4458
|
+
];
|
|
4459
|
+
/** Short plain-text lines describing the arguments that decide what runs. */
|
|
4460
|
+
export function describeOperation(args) {
|
|
4461
|
+
const lines = [];
|
|
4462
|
+
if (!args || typeof args !== "object")
|
|
4463
|
+
return lines;
|
|
4464
|
+
for (const key of DESCRIBED_ARG_KEYS) {
|
|
4465
|
+
const value = args[key];
|
|
4466
|
+
if (value === undefined || value === null || value === "")
|
|
4467
|
+
continue;
|
|
4468
|
+
let text = typeof value === "string" ? value : JSON.stringify(value);
|
|
4469
|
+
if (text.length > 600)
|
|
4470
|
+
text = `${text.substring(0, 600)}... (${text.length} characters)`;
|
|
4471
|
+
lines.push(`${key}: ${text}`);
|
|
3513
4472
|
}
|
|
4473
|
+
return lines;
|
|
3514
4474
|
}
|
|
3515
4475
|
function isMissingColumnError(err, column) {
|
|
3516
4476
|
const message = String(err?.message || err || "").toUpperCase();
|
|
3517
4477
|
return message.includes("42703") && message.includes(column.toUpperCase());
|
|
3518
4478
|
}
|
|
3519
|
-
function requireIfsPath(value, label) {
|
|
4479
|
+
export function requireIfsPath(value, label) {
|
|
3520
4480
|
const pathValue = String(value || "").trim();
|
|
3521
4481
|
if (!pathValue.startsWith("/")) {
|
|
3522
4482
|
throw new Error(`Invalid ${label}: must be an absolute IFS path`);
|
|
@@ -3533,10 +4493,10 @@ function requireIfsJournalObjectType(value) {
|
|
|
3533
4493
|
}
|
|
3534
4494
|
return objectType;
|
|
3535
4495
|
}
|
|
3536
|
-
function quoteClString(value) {
|
|
4496
|
+
export function quoteClString(value) {
|
|
3537
4497
|
return `'${String(value).replace(/'/g, "''")}'`;
|
|
3538
4498
|
}
|
|
3539
|
-
function requireIbmObjectName(value, label) {
|
|
4499
|
+
export function requireIbmObjectName(value, label) {
|
|
3540
4500
|
const name = String(value || "").trim().toUpperCase();
|
|
3541
4501
|
if (!/^[A-Z#@$][A-Z0-9#@$]{0,9}$/.test(name)) {
|
|
3542
4502
|
throw new Error(`Invalid ${label}: ${value}`);
|
|
@@ -3678,14 +4638,58 @@ function requiresApproval(policy, operation) {
|
|
|
3678
4638
|
const requires = policy.requireApprovalFor || [];
|
|
3679
4639
|
return requires.includes(operation);
|
|
3680
4640
|
}
|
|
4641
|
+
/** Returns the reason an operation is denied by the command policy, if any. */
|
|
3681
4642
|
function isDeniedByPolicy(policy, operation, args) {
|
|
3682
|
-
const denyCommands = (policy.denyCommands || []).map(v => v.toLowerCase());
|
|
3683
4643
|
if (operation === "cl.run" && args?.command) {
|
|
3684
|
-
const
|
|
3685
|
-
|
|
3686
|
-
|
|
4644
|
+
const environment = String(args.environment || "ile");
|
|
4645
|
+
return environment === "ile"
|
|
4646
|
+
? checkClCommand(policy, String(args.command))
|
|
4647
|
+
: checkShellCommand(policy, String(args.command));
|
|
4648
|
+
}
|
|
4649
|
+
if (typeof args?.sql === "string") {
|
|
4650
|
+
return checkSqlEmbeddedCl(policy, args.sql);
|
|
4651
|
+
}
|
|
4652
|
+
return undefined;
|
|
4653
|
+
}
|
|
4654
|
+
/** Library, IFS path and embedded-CL policy checks that apply to every tool. */
|
|
4655
|
+
async function enforceResourcePolicy(ctx, toolName, args) {
|
|
4656
|
+
const resolver = ctx;
|
|
4657
|
+
const policy = typeof resolver.resolvePolicy === "function"
|
|
4658
|
+
? await resolver.resolvePolicy(args?.connectionName)
|
|
4659
|
+
: typeof resolver.getPolicy === "function" ? resolver.getPolicy(args?.connectionName) : undefined;
|
|
4660
|
+
if (!policy)
|
|
4661
|
+
return;
|
|
4662
|
+
const reason = checkResourcePolicy(policy, toolName, args);
|
|
4663
|
+
if (reason) {
|
|
4664
|
+
throw new Error(`Blocked by policy: ${toolName} (${reason})`);
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4667
|
+
/** Check the policy against an action's expanded command before it runs. */
|
|
4668
|
+
export function actionCommandGuard(ctx, environment, args) {
|
|
4669
|
+
return async (expanded) => {
|
|
4670
|
+
const policy = ctx.getPolicy(args?.connectionName);
|
|
4671
|
+
const env = environment || "ile";
|
|
4672
|
+
const reason = env === "ile"
|
|
4673
|
+
? checkClCommands(policy, expanded)
|
|
4674
|
+
: checkShellCommand(policy, expanded);
|
|
4675
|
+
if (reason)
|
|
4676
|
+
throw new Error(`Blocked by policy: actions.run (${reason})`);
|
|
4677
|
+
if (policy.profile === "guarded" && (env !== "ile" || clStartsShell(expanded))) {
|
|
4678
|
+
await requireConfirmation(ctx, policy, "shell.run", { ...args, command: expanded, environment: env }, false);
|
|
4679
|
+
}
|
|
4680
|
+
return expanded;
|
|
4681
|
+
};
|
|
4682
|
+
}
|
|
4683
|
+
/** Resolve a local path and refuse it outside the allowed local roots. */
|
|
4684
|
+
export async function guardLocalPath(ctx, value, mode) {
|
|
4685
|
+
let settingsRoots;
|
|
4686
|
+
try {
|
|
4687
|
+
settingsRoots = (await ctx.store.getSettings())?.allowedLocalRoots;
|
|
4688
|
+
}
|
|
4689
|
+
catch {
|
|
4690
|
+
settingsRoots = undefined;
|
|
3687
4691
|
}
|
|
3688
|
-
return
|
|
4692
|
+
return resolveAllowedLocalPath(value, mode, configuredLocalRoots(settingsRoots));
|
|
3689
4693
|
}
|
|
3690
4694
|
const APPROVAL_HINT_TOOLS = new Set([
|
|
3691
4695
|
"ibmi.qsys.members.write",
|
|
@@ -3742,11 +4746,32 @@ function withGuardedApprovalHint(tool) {
|
|
|
3742
4746
|
if (!schema.properties.approve) {
|
|
3743
4747
|
schema.properties.approve = {
|
|
3744
4748
|
type: "boolean",
|
|
3745
|
-
description: "
|
|
4749
|
+
description: "Only used when the connection's approvalMode is \"agent\". By default the user is asked to confirm guarded operations and this flag is ignored."
|
|
3746
4750
|
};
|
|
3747
4751
|
}
|
|
3748
4752
|
return { ...tool, inputSchema: schema };
|
|
3749
4753
|
}
|
|
4754
|
+
/** Tools outside the qsys/search/source families that also accept connectionName. */
|
|
4755
|
+
const CONNECTION_NAME_TOOLS = new Set([
|
|
4756
|
+
"ibmi.find.ifs",
|
|
4757
|
+
"ibmi.actions.run",
|
|
4758
|
+
"ibmi.libl.validate",
|
|
4759
|
+
"ibmi.resolve.path",
|
|
4760
|
+
"ibmi.debug.status",
|
|
4761
|
+
"ibmi.journal.objects.list"
|
|
4762
|
+
]);
|
|
4763
|
+
function withSourceConnectionName(tool) {
|
|
4764
|
+
const accepts = /^ibmi\.(?:qsys|search|source|ifs|deploy)\./.test(tool.name) || CONNECTION_NAME_TOOLS.has(tool.name);
|
|
4765
|
+
if (!accepts)
|
|
4766
|
+
return tool;
|
|
4767
|
+
const schema = tool.inputSchema || { type: "object", properties: {} };
|
|
4768
|
+
if (!schema.properties)
|
|
4769
|
+
schema.properties = {};
|
|
4770
|
+
if (!schema.properties.connectionName) {
|
|
4771
|
+
schema.properties.connectionName = { type: "string", description: "Saved IBM i connection name." };
|
|
4772
|
+
}
|
|
4773
|
+
return { ...tool, inputSchema: schema };
|
|
4774
|
+
}
|
|
3750
4775
|
function withStrictInputSchema(tool) {
|
|
3751
4776
|
const schema = strictifySchema(tool.inputSchema || { type: "object", properties: {} });
|
|
3752
4777
|
return { ...tool, inputSchema: schema };
|
|
@@ -3775,33 +4800,10 @@ function isObjectSchema(schema) {
|
|
|
3775
4800
|
return schema.type.includes("object");
|
|
3776
4801
|
return schema.type === "object";
|
|
3777
4802
|
}
|
|
3778
|
-
function resolveIfsWriteOperation(pathValue) {
|
|
4803
|
+
export function resolveIfsWriteOperation(pathValue) {
|
|
3779
4804
|
const target = String(pathValue || "");
|
|
3780
4805
|
if (/^\/qsys\.lib\//i.test(target)) {
|
|
3781
4806
|
return "qsys.write";
|
|
3782
4807
|
}
|
|
3783
4808
|
return "ifs.write";
|
|
3784
4809
|
}
|
|
3785
|
-
function parseEvfevent(content) {
|
|
3786
|
-
const diagnostics = [];
|
|
3787
|
-
for (const rawLine of content.split(/\r?\n/)) {
|
|
3788
|
-
const line = rawLine.trimEnd();
|
|
3789
|
-
if (!line.trim())
|
|
3790
|
-
continue;
|
|
3791
|
-
const match = line.match(/^\s*(\d+)\s+(\d+)\s+([A-Z0-9]+)\s+(INFO|WARN|ERROR)?\s*(.*)$/i);
|
|
3792
|
-
if (match) {
|
|
3793
|
-
diagnostics.push({
|
|
3794
|
-
source: "evfevent",
|
|
3795
|
-
line: Number(match[1]),
|
|
3796
|
-
column: Number(match[2]),
|
|
3797
|
-
code: match[3],
|
|
3798
|
-
severity: (match[4] || "ERROR").toUpperCase(),
|
|
3799
|
-
message: match[5] || line
|
|
3800
|
-
});
|
|
3801
|
-
}
|
|
3802
|
-
else {
|
|
3803
|
-
diagnostics.push({ source: "evfevent", severity: inferSeverity(line), message: line });
|
|
3804
|
-
}
|
|
3805
|
-
}
|
|
3806
|
-
return diagnostics;
|
|
3807
|
-
}
|