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