@ffschrattenecker/tm1-mcp-server 6.0.0 → 6.1.1
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/CHANGELOG.md +76 -1
- package/README.md +5 -2
- package/dist/config.d.ts +3 -1
- package/dist/config.js +50 -34
- package/dist/index.js +4 -1
- package/dist/tm1-client.d.ts +6 -0
- package/dist/tm1-client.js +8 -0
- package/dist/tools/analysis/analyze-object-usage.d.ts +10 -0
- package/dist/tools/analysis/analyze-object-usage.js +37 -33
- package/dist/tools/analysis/delete-impact.d.ts +13 -0
- package/dist/tools/analysis/delete-impact.js +30 -0
- package/dist/tools/celldata/write-cells.js +37 -2
- package/dist/tools/confirm.d.ts +4 -1
- package/dist/tools/confirm.js +11 -1
- package/dist/tools/dimension-management/delete-dimension.js +18 -4
- package/dist/tools/model-building/delete-cube.js +18 -4
- package/dist/tools/model-building/set-cube-rules.js +13 -1
- package/dist/tools/operations/get-server-info.js +10 -1
- package/dist/tools/schemas/items-monitoring.d.ts +11 -0
- package/dist/tools/schemas/items-monitoring.js +9 -1
- package/dist/tools/schemas/items-processes.d.ts +3 -0
- package/dist/tools/schemas/items-processes.js +2 -1
- package/dist/tools/ti-development/diff-processes.d.ts +42 -0
- package/dist/tools/ti-development/diff-processes.js +4 -4
- package/dist/tools/ti-development/execute-process.js +27 -2
- package/dist/tools/ti-development/preflight.d.ts +24 -0
- package/dist/tools/ti-development/preflight.js +32 -0
- package/dist/tools/ti-development/upsert-process.js +99 -10
- package/npm-shrinkwrap.json +4586 -0
- package/package.json +7 -6
|
@@ -3,22 +3,36 @@ import { defineTool } from "../define-tool.js";
|
|
|
3
3
|
import { DESTRUCTIVE } from "../annotations.js";
|
|
4
4
|
import { MutationResultSchema } from "../schemas/items.js";
|
|
5
5
|
import { actionResponse } from "../format.js";
|
|
6
|
-
import {
|
|
6
|
+
import { DRY_RUN_CONFIRM_SCHEMA, requireConfirm } from "../confirm.js";
|
|
7
7
|
import { withToolHint } from "../error-format.js";
|
|
8
|
+
import { deleteImpact } from "../analysis/delete-impact.js";
|
|
8
9
|
export const registerDeleteCube = defineTool({
|
|
9
10
|
name: "tm1_delete_cube",
|
|
10
11
|
description: [
|
|
11
12
|
"Delete a TM1 cube and all its data. This action is irreversible.",
|
|
12
13
|
"Safety: pass confirm=<cube name verbatim>. Mismatched confirm rejects the call.",
|
|
13
|
-
"Before:
|
|
14
|
+
"Before: dryRun=true lists every process or rule that references the cube, in one call; tm1_get_cube_stats sizes the data loss.",
|
|
14
15
|
],
|
|
15
16
|
annotations: DESTRUCTIVE,
|
|
16
17
|
output: MutationResultSchema,
|
|
17
18
|
input: {
|
|
18
19
|
cubeName: z.string().describe("Cube name (case-sensitive)"),
|
|
19
|
-
|
|
20
|
+
dryRun: z
|
|
21
|
+
.boolean()
|
|
22
|
+
.optional()
|
|
23
|
+
.default(false)
|
|
24
|
+
.describe("Delete nothing: return what references the cube (processes and rules, per source). Needs no confirm and is not one."),
|
|
25
|
+
...DRY_RUN_CONFIRM_SCHEMA,
|
|
20
26
|
},
|
|
21
|
-
handler: async ({ cubeName, confirm }, tm1Client) => {
|
|
27
|
+
handler: async ({ cubeName, dryRun, confirm }, tm1Client) => {
|
|
28
|
+
if (dryRun) {
|
|
29
|
+
return actionResponse({
|
|
30
|
+
success: true,
|
|
31
|
+
dryRun: true,
|
|
32
|
+
cubeName,
|
|
33
|
+
impact: await deleteImpact(tm1Client, "cube", cubeName),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
22
36
|
requireConfirm(confirm, cubeName, "cube");
|
|
23
37
|
await withToolHint(tm1Client.cubes.delete(cubeName), "Cube delete failed. Common causes: cube still referenced by TI processes / chores / rules (run tm1_analyze_object_usage to inspect), insufficient permissions, or cube does not exist (tm1_list_cubes to verify name + casing).");
|
|
24
38
|
return actionResponse({ success: true, cubeName });
|
|
@@ -14,7 +14,7 @@ export const registerSetCubeRules = defineTool({
|
|
|
14
14
|
"Create or replace the rules for a TM1 cube.",
|
|
15
15
|
"SKIPCHECK; belongs at the top and FEEDERS; before all feeder definitions — SKIPCHECK is what makes feeders take effect, so rules with feeders need it.",
|
|
16
16
|
"Pass exactly one source: rules (the full text — replaces everything), edits (find/replace patch against the current text; each find must match exactly once, else nothing is written), or filePath (full text from a host file under TM1_LOCAL_FILE_ROOT).",
|
|
17
|
-
"The full resulting text is syntax-checked before anything is written (preflight).
|
|
17
|
+
"The full resulting text is syntax-checked before anything is written (preflight). The stored text is read back after writing (verified.textMatches), so no separate tm1_get_cube_rules is needed; the callgraph cache is dropped automatically.",
|
|
18
18
|
],
|
|
19
19
|
annotations: IDEMPOTENT_DESTRUCTIVE,
|
|
20
20
|
output: MutationResultSchema,
|
|
@@ -65,6 +65,17 @@ export const registerSetCubeRules = defineTool({
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
await withToolHint(tm1Client.cubes.updateRules(cubeName, text), `Inspect details for the offending line.`);
|
|
68
|
+
// Read back what TM1 stored, compared with line endings normalized. The
|
|
69
|
+
// write has landed by now, so a failed read-back is reported, not thrown.
|
|
70
|
+
const norm = (x) => x.replace(/\r\n/g, "\n").trimEnd();
|
|
71
|
+
let verified;
|
|
72
|
+
try {
|
|
73
|
+
const stored = (await tm1Client.cubes.getRules(cubeName)).rulesText;
|
|
74
|
+
verified = { textMatches: norm(stored) === norm(text) };
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
verified = { readBackError: e.message };
|
|
78
|
+
}
|
|
68
79
|
const lineCount = text.split("\n").length;
|
|
69
80
|
// Rule changes shift call edges (DB(), feeders) — drop callgraph TTL early.
|
|
70
81
|
const { cleared: callgraphEntriesCleared } = invalidateCallgraphCache();
|
|
@@ -75,6 +86,7 @@ export const registerSetCubeRules = defineTool({
|
|
|
75
86
|
lineCount,
|
|
76
87
|
...(edits !== undefined ? { editsApplied: edits.length } : {}),
|
|
77
88
|
callgraphEntriesCleared,
|
|
89
|
+
verified,
|
|
78
90
|
});
|
|
79
91
|
},
|
|
80
92
|
});
|
|
@@ -27,10 +27,19 @@ export const registerGetServerInfo = defineTool({
|
|
|
27
27
|
handler: async ({ format }, tm1Client) => {
|
|
28
28
|
const info = await tm1Client.server.getInfo();
|
|
29
29
|
const x = info.extra ?? {};
|
|
30
|
+
const access = tm1Client.access;
|
|
30
31
|
const payload = {
|
|
31
32
|
// The MCP server's own package identity, so a client can gate on it
|
|
32
33
|
// without parsing the initialize handshake.
|
|
33
|
-
|
|
34
|
+
// mode/environment let a client read what this connection may do and
|
|
35
|
+
// where it points, instead of inferring it from which tools are listed.
|
|
36
|
+
mcpServer: {
|
|
37
|
+
name: NAME,
|
|
38
|
+
version: VERSION,
|
|
39
|
+
mode: access.mode,
|
|
40
|
+
environment: access.environment ?? "unspecified",
|
|
41
|
+
...(access.modeReason ? { modeReason: access.modeReason } : {}),
|
|
42
|
+
},
|
|
34
43
|
serverName: info.serverName,
|
|
35
44
|
productVersion: info.productVersion,
|
|
36
45
|
productEdition: info.productEdition,
|
|
@@ -4,6 +4,17 @@ export declare const ServerInfoSchema: z.ZodObject<{
|
|
|
4
4
|
mcpServer: z.ZodOptional<z.ZodObject<{
|
|
5
5
|
name: z.ZodString;
|
|
6
6
|
version: z.ZodString;
|
|
7
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
8
|
+
readwrite: "readwrite";
|
|
9
|
+
readonly: "readonly";
|
|
10
|
+
}>>;
|
|
11
|
+
environment: z.ZodOptional<z.ZodEnum<{
|
|
12
|
+
dev: "dev";
|
|
13
|
+
test: "test";
|
|
14
|
+
prod: "prod";
|
|
15
|
+
unspecified: "unspecified";
|
|
16
|
+
}>>;
|
|
17
|
+
modeReason: z.ZodOptional<z.ZodString>;
|
|
7
18
|
}, z.core.$strip>>;
|
|
8
19
|
serverName: z.ZodString;
|
|
9
20
|
productVersion: z.ZodString;
|
|
@@ -7,7 +7,15 @@ export { AuditLogDetailSchema, ErrorLogFileSchema, JobSchema, MessageLogEntrySch
|
|
|
7
7
|
import { AuditLogDetailSchema } from "../../schemas/monitoring.js";
|
|
8
8
|
export const ServerInfoSchema = z
|
|
9
9
|
.object({
|
|
10
|
-
mcpServer: z
|
|
10
|
+
mcpServer: z
|
|
11
|
+
.object({
|
|
12
|
+
name: z.string(),
|
|
13
|
+
version: z.string(),
|
|
14
|
+
mode: z.enum(["readwrite", "readonly"]).optional(),
|
|
15
|
+
environment: z.enum(["dev", "test", "prod", "unspecified"]).optional(),
|
|
16
|
+
modeReason: z.string().optional(),
|
|
17
|
+
})
|
|
18
|
+
.optional(),
|
|
11
19
|
serverName: z.string(),
|
|
12
20
|
productVersion: z.string(),
|
|
13
21
|
productEdition: z.string().optional(),
|
|
@@ -154,12 +154,15 @@ export declare const UpsertProcessResultSchema: z.ZodObject<{
|
|
|
154
154
|
action: z.ZodEnum<{
|
|
155
155
|
created: "created";
|
|
156
156
|
updated: "updated";
|
|
157
|
+
wouldCreate: "wouldCreate";
|
|
158
|
+
wouldUpdate: "wouldUpdate";
|
|
157
159
|
}>;
|
|
158
160
|
appliedSteps: z.ZodArray<z.ZodString>;
|
|
159
161
|
backup: z.ZodOptional<z.ZodObject<{
|
|
160
162
|
json: z.ZodString;
|
|
161
163
|
ti: z.ZodString;
|
|
162
164
|
}, z.core.$strip>>;
|
|
165
|
+
dryRun: z.ZodOptional<z.ZodBoolean>;
|
|
163
166
|
}, z.core.$loose>;
|
|
164
167
|
export declare const InstallProBundleResultSchema: z.ZodObject<{
|
|
165
168
|
directory: z.ZodString;
|
|
@@ -102,9 +102,10 @@ const ProcessBackupSchema = z.object({ json: z.string(), ti: z.string() });
|
|
|
102
102
|
export const UpsertProcessResultSchema = z
|
|
103
103
|
.object({
|
|
104
104
|
processName: z.string(),
|
|
105
|
-
action: z.enum(["created", "updated"]),
|
|
105
|
+
action: z.enum(["created", "updated", "wouldCreate", "wouldUpdate"]),
|
|
106
106
|
appliedSteps: z.array(z.string()),
|
|
107
107
|
backup: ProcessBackupSchema.optional(),
|
|
108
|
+
dryRun: z.boolean().optional(),
|
|
108
109
|
})
|
|
109
110
|
.passthrough();
|
|
110
111
|
const InstallBundleEntrySchema = z
|
|
@@ -1,2 +1,44 @@
|
|
|
1
|
+
import type { ProcessParameter, ProcessVariable, DataSource } from "../../types.js";
|
|
2
|
+
interface DiffHunk {
|
|
3
|
+
startA: number;
|
|
4
|
+
countA: number;
|
|
5
|
+
startB: number;
|
|
6
|
+
countB: number;
|
|
7
|
+
lines: Array<{
|
|
8
|
+
type: " " | "+" | "-";
|
|
9
|
+
text: string;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
export declare function tabCodeDiff(codeA: string, codeB: string, contextLines: number): {
|
|
13
|
+
identical: boolean;
|
|
14
|
+
linesA: number;
|
|
15
|
+
linesB: number;
|
|
16
|
+
hunks: DiffHunk[];
|
|
17
|
+
};
|
|
18
|
+
export declare function diffParams(a: ProcessParameter[], b: ProcessParameter[]): {
|
|
19
|
+
identical: boolean;
|
|
20
|
+
added: string[];
|
|
21
|
+
removed: string[];
|
|
22
|
+
changed: {
|
|
23
|
+
name: string;
|
|
24
|
+
a: ProcessParameter;
|
|
25
|
+
b: ProcessParameter;
|
|
26
|
+
}[];
|
|
27
|
+
};
|
|
28
|
+
export declare function diffVars(a: ProcessVariable[], b: ProcessVariable[]): {
|
|
29
|
+
identical: boolean;
|
|
30
|
+
added: string[];
|
|
31
|
+
removed: string[];
|
|
32
|
+
changed: {
|
|
33
|
+
name: string;
|
|
34
|
+
a: ProcessVariable;
|
|
35
|
+
b: ProcessVariable;
|
|
36
|
+
}[];
|
|
37
|
+
};
|
|
38
|
+
export declare function diffDs(a: DataSource, b: DataSource): {
|
|
39
|
+
identical: boolean;
|
|
40
|
+
differences: string[];
|
|
41
|
+
};
|
|
1
42
|
export declare const registerDiffProcesses: import("../define-tool.js").ToolRegistrar;
|
|
43
|
+
export {};
|
|
2
44
|
//# sourceMappingURL=diff-processes.d.ts.map
|
|
@@ -105,7 +105,7 @@ function buildHunks(ops, contextLines) {
|
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
107
|
const NORM = (s) => s.replace(/\r\n/g, "\n").trimEnd();
|
|
108
|
-
function tabCodeDiff(codeA, codeB, contextLines) {
|
|
108
|
+
export function tabCodeDiff(codeA, codeB, contextLines) {
|
|
109
109
|
const na = NORM(codeA);
|
|
110
110
|
const nb = NORM(codeB);
|
|
111
111
|
if (na === nb) {
|
|
@@ -119,7 +119,7 @@ function tabCodeDiff(codeA, codeB, contextLines) {
|
|
|
119
119
|
return { identical: false, linesA: a.length, linesB: b.length, hunks };
|
|
120
120
|
}
|
|
121
121
|
// ── param / var / datasource diff ────────────────────────────────────────────
|
|
122
|
-
function diffParams(a, b) {
|
|
122
|
+
export function diffParams(a, b) {
|
|
123
123
|
const ma = new Map(a.map((p) => [p.name, p]));
|
|
124
124
|
const mb = new Map(b.map((p) => [p.name, p]));
|
|
125
125
|
const added = [];
|
|
@@ -147,7 +147,7 @@ function diffParams(a, b) {
|
|
|
147
147
|
changed,
|
|
148
148
|
};
|
|
149
149
|
}
|
|
150
|
-
function diffVars(a, b) {
|
|
150
|
+
export function diffVars(a, b) {
|
|
151
151
|
const ma = new Map(a.map((v) => [v.name, v]));
|
|
152
152
|
const mb = new Map(b.map((v) => [v.name, v]));
|
|
153
153
|
const added = [];
|
|
@@ -204,7 +204,7 @@ function diffIgnoredColumns(a, b) {
|
|
|
204
204
|
renamed,
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
|
-
function diffDs(a, b) {
|
|
207
|
+
export function diffDs(a, b) {
|
|
208
208
|
const diffs = [];
|
|
209
209
|
const fields = [
|
|
210
210
|
"type",
|
|
@@ -5,6 +5,7 @@ import { CONFIRM_SCHEMA, requireConfirm } from "../confirm.js";
|
|
|
5
5
|
import { DESTRUCTIVE } from "../annotations.js";
|
|
6
6
|
import { ProcessResultSchema } from "../schemas/items.js";
|
|
7
7
|
import { defineTool } from "../define-tool.js";
|
|
8
|
+
import { tailLines } from "../operations/error-log-helpers.js";
|
|
8
9
|
/**
|
|
9
10
|
* Client-abort recovery hint, branched by TM1 major version: v11 exposes
|
|
10
11
|
* server threads (tm1_list_threads / tm1_cancel_thread), v12 exposes jobs
|
|
@@ -15,13 +16,24 @@ export function abortHint(version) {
|
|
|
15
16
|
const cancel = version === 12 ? "tm1_cancel_job" : "tm1_cancel_thread";
|
|
16
17
|
return `Request aborted by the client — the process was NOT confirmed failed and may still be executing. Use ${monitor} to check for it and ${cancel} to stop it. Do NOT blindly re-run: that risks a duplicate execution.`;
|
|
17
18
|
}
|
|
19
|
+
const ERROR_LOG_TAIL_LINES = 40;
|
|
20
|
+
async function fetchErrorLog(tm1Client, filename) {
|
|
21
|
+
try {
|
|
22
|
+
const raw = await tm1Client.server.getErrorLogContent(filename);
|
|
23
|
+
const { body, truncated, totalLines } = tailLines(raw, ERROR_LOG_TAIL_LINES);
|
|
24
|
+
return { filename, totalLines, truncated, content: body };
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
return { filename, fetchError: e.message };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
18
30
|
export const registerExecuteProcess = defineTool({
|
|
19
31
|
name: "tm1_execute_process",
|
|
20
32
|
description: [
|
|
21
33
|
"Execute a TurboIntegrator process on the TM1 server with optional parameters.",
|
|
22
34
|
"Non-idempotent: each call re-runs the process — do not retry blindly on transport errors without checking server state.",
|
|
23
35
|
"Before: tm1_check_process_code (syntax) and/or tm1_compile_process (full compile). Discover required params with tm1_get_process_parameters.",
|
|
24
|
-
"
|
|
36
|
+
"When TM1 wrote an error log for the run (failure or minor errors), its last 40 lines come back as errorLog. For cascade siblings and older logs use tm1_diagnose_process_error.",
|
|
25
37
|
],
|
|
26
38
|
annotations: DESTRUCTIVE,
|
|
27
39
|
output: ProcessResultSchema,
|
|
@@ -75,8 +87,21 @@ export const registerExecuteProcess = defineTool({
|
|
|
75
87
|
signal: extra?.signal,
|
|
76
88
|
...(timeoutMs ? { timeoutMs } : {}),
|
|
77
89
|
}), `Process '${processName}' failed at runtime. Inspect cascade with tm1_diagnose_process_error(processName='${processName}', includeRelated=true). Verify parameter shape via tm1_get_process_parameters; check syntax with tm1_compile_process before re-running.`);
|
|
90
|
+
// TM1 names the run's own error log when it wrote one (minor errors
|
|
91
|
+
// included). Attach its tail so judging the run takes no extra call.
|
|
92
|
+
const errorLog = result.errorLogFile
|
|
93
|
+
? await fetchErrorLog(tm1Client, result.errorLogFile)
|
|
94
|
+
: undefined;
|
|
78
95
|
return {
|
|
79
|
-
content: [
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: "text",
|
|
99
|
+
text: JSON.stringify({
|
|
100
|
+
...result,
|
|
101
|
+
...(errorLog ? { errorLog } : {}),
|
|
102
|
+
}),
|
|
103
|
+
},
|
|
104
|
+
],
|
|
80
105
|
// A TI process that ran but reported failure is a tool failure, not a
|
|
81
106
|
// successful call carrying success:false. Flag isError so agents that
|
|
82
107
|
// branch on the MCP error signal don't silently treat it as success;
|
|
@@ -36,4 +36,28 @@ export declare function preflightResult(failure: PreflightFailure): {
|
|
|
36
36
|
}[];
|
|
37
37
|
isError: true;
|
|
38
38
|
};
|
|
39
|
+
export interface PreflightChecks {
|
|
40
|
+
ok: boolean;
|
|
41
|
+
syntax: {
|
|
42
|
+
ok: boolean;
|
|
43
|
+
errors: Array<{
|
|
44
|
+
procedure?: string | undefined;
|
|
45
|
+
lineNumber?: number | undefined;
|
|
46
|
+
message: string;
|
|
47
|
+
}>;
|
|
48
|
+
};
|
|
49
|
+
references: {
|
|
50
|
+
ok: boolean;
|
|
51
|
+
unresolved: number;
|
|
52
|
+
issues: RefIssue[];
|
|
53
|
+
partial: boolean;
|
|
54
|
+
unresolvableArgs: number;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Both checks, neither short-circuiting the other: the dryRun report the
|
|
59
|
+
* user approves from. runPreflight stops at the first failure because the
|
|
60
|
+
* install is refused either way; a pre-approval report needs every finding.
|
|
61
|
+
*/
|
|
62
|
+
export declare function runChecks(tm1Client: TM1Client, p: PreflightPayload): Promise<PreflightChecks>;
|
|
39
63
|
//# sourceMappingURL=preflight.d.ts.map
|
|
@@ -53,4 +53,36 @@ export function preflightResult(failure) {
|
|
|
53
53
|
isError: true,
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Both checks, neither short-circuiting the other: the dryRun report the
|
|
58
|
+
* user approves from. runPreflight stops at the first failure because the
|
|
59
|
+
* install is refused either way; a pre-approval report needs every finding.
|
|
60
|
+
*/
|
|
61
|
+
export async function runChecks(tm1Client, p) {
|
|
62
|
+
const [check, refs] = await Promise.all([
|
|
63
|
+
tm1Client.processes.check({
|
|
64
|
+
name: p.name,
|
|
65
|
+
prolog: p.prolog,
|
|
66
|
+
metadata: p.metadata,
|
|
67
|
+
data: p.data,
|
|
68
|
+
epilog: p.epilog,
|
|
69
|
+
...(p.parameters !== undefined ? { parameters: p.parameters } : {}),
|
|
70
|
+
...(p.variables !== undefined ? { variables: p.variables } : {}),
|
|
71
|
+
...(p.dataSource !== undefined ? { dataSource: p.dataSource } : {}),
|
|
72
|
+
}),
|
|
73
|
+
checkProcessRefs(tm1Client, p),
|
|
74
|
+
]);
|
|
75
|
+
const references = {
|
|
76
|
+
ok: refs.unresolved === 0,
|
|
77
|
+
unresolved: refs.unresolved,
|
|
78
|
+
issues: refs.issues,
|
|
79
|
+
partial: refs.partial,
|
|
80
|
+
unresolvableArgs: refs.unresolvableArgs,
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
ok: check.success && references.ok,
|
|
84
|
+
syntax: { ok: check.success, errors: check.errors },
|
|
85
|
+
references,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
56
88
|
//# sourceMappingURL=preflight.js.map
|
|
@@ -6,7 +6,9 @@ import { IDEMPOTENT_DESTRUCTIVE } from "../annotations.js";
|
|
|
6
6
|
import { UpsertProcessResultSchema } from "../schemas/items.js";
|
|
7
7
|
import { defineTool } from "../define-tool.js";
|
|
8
8
|
import { OVERWRITE_CONFIRM_SCHEMA, requireOverwriteConfirm, } from "../confirm.js";
|
|
9
|
-
import { preflightResult, runPreflight } from "./preflight.js";
|
|
9
|
+
import { preflightResult, runChecks, runPreflight, } from "./preflight.js";
|
|
10
|
+
import { diffDs, diffParams, diffVars, tabCodeDiff } from "./diff-processes.js";
|
|
11
|
+
import { maskCode } from "../../lib/mask-secrets.js";
|
|
10
12
|
import { backupProcess } from "./process-backup.js";
|
|
11
13
|
// The same data source shape the git round-trip and check_process_code use.
|
|
12
14
|
// This tool used to carry its own copy, which had drifted: it was missing
|
|
@@ -20,6 +22,7 @@ const parameterSchema = z.object({
|
|
|
20
22
|
defaultValue: z.union([z.string(), z.number()]),
|
|
21
23
|
prompt: z.string().optional(),
|
|
22
24
|
});
|
|
25
|
+
const TABS = ["prolog", "metadata", "data", "epilog"];
|
|
23
26
|
const variableSchema = z.object({
|
|
24
27
|
name: z.string(),
|
|
25
28
|
type: z.enum(["String", "Numeric"]),
|
|
@@ -57,8 +60,13 @@ export const registerUpsertProcess = defineTool({
|
|
|
57
60
|
.optional()
|
|
58
61
|
.default(false)
|
|
59
62
|
.describe("After deploy, run tm1.Compile and include the result in the response (compile: {ok, errorCount, errors}). Off by default — compile holds a brief lock on the process and serializes badly under bulk-deploy."),
|
|
63
|
+
dryRun: z
|
|
64
|
+
.boolean()
|
|
65
|
+
.optional()
|
|
66
|
+
.default(false)
|
|
67
|
+
.describe("Write nothing. Run the syntax AND reference check on the process as it would be after this call (both reported, neither stops the other) and diff it against the installed version (credentials masked). Needs no confirm and is not one: the real call still needs it. Replaces a separate check_process_code + validate_process_refs + diff before the risk assessment."),
|
|
60
68
|
},
|
|
61
|
-
handler: async ({ processName, prolog, metadata, data, epilog, parameters, variables, dataSource, hasSecurityAccess, mode, preflight, autoCompile, confirm, }, tm1Client) => {
|
|
69
|
+
handler: async ({ processName, prolog, metadata, data, epilog, parameters, variables, dataSource, hasSecurityAccess, mode, preflight, autoCompile, dryRun, confirm, }, tm1Client) => {
|
|
62
70
|
const trail = [];
|
|
63
71
|
const exists = await tm1Client.processes.exists(processName);
|
|
64
72
|
if (mode === "create" && exists) {
|
|
@@ -73,16 +81,13 @@ export const registerUpsertProcess = defineTool({
|
|
|
73
81
|
message: `Process '${processName}' does not exist; mode=update`,
|
|
74
82
|
});
|
|
75
83
|
}
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (preflight) {
|
|
80
|
-
// Check the process as it will stand after this call, not the fields
|
|
81
|
-
// the caller happened to send: an omitted tab keeps its installed code.
|
|
84
|
+
// The process as it will stand after this call, not the fields the
|
|
85
|
+
// caller happened to send: an omitted tab keeps its installed code.
|
|
86
|
+
const resolve = async () => {
|
|
82
87
|
const current = exists
|
|
83
88
|
? await tm1Client.processes.getCode(processName)
|
|
84
89
|
: { prolog: "", metadata: "", data: "", epilog: "" };
|
|
85
|
-
|
|
90
|
+
return {
|
|
86
91
|
name: processName,
|
|
87
92
|
prolog: prolog ?? current.prolog,
|
|
88
93
|
metadata: metadata ?? current.metadata,
|
|
@@ -96,7 +101,79 @@ export const registerUpsertProcess = defineTool({
|
|
|
96
101
|
? await tm1Client.processes.getVariables(processName)
|
|
97
102
|
: [],
|
|
98
103
|
...(dataSource !== undefined ? { dataSource } : {}),
|
|
99
|
-
}
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
if (dryRun) {
|
|
107
|
+
// Deliberately before the confirm gate: a dry run writes nothing, and
|
|
108
|
+
// it is what the risk assessment is built from.
|
|
109
|
+
const payload = await resolve();
|
|
110
|
+
const [checks, installed, installedParams] = await Promise.all([
|
|
111
|
+
runChecks(tm1Client, payload),
|
|
112
|
+
exists
|
|
113
|
+
? tm1Client.processes.getCode(processName)
|
|
114
|
+
: Promise.resolve({ prolog: "", metadata: "", data: "", epilog: "" }),
|
|
115
|
+
exists
|
|
116
|
+
? tm1Client.processes.getParameters(processName)
|
|
117
|
+
: Promise.resolve([]),
|
|
118
|
+
]);
|
|
119
|
+
const tabs = Object.fromEntries(TABS.map((t) => [
|
|
120
|
+
t,
|
|
121
|
+
tabCodeDiff(maskCode(installed[t] ?? ""), maskCode(payload[t]), 3),
|
|
122
|
+
]));
|
|
123
|
+
const params = diffParams(installedParams, payload.parameters ?? []);
|
|
124
|
+
// Variables and datasource change only when the caller sends them; a
|
|
125
|
+
// datasource swap must not come back as "identical".
|
|
126
|
+
const sendsVars = variables !== undefined && variables.length > 0;
|
|
127
|
+
const [installedVars, installedDs] = await Promise.all([
|
|
128
|
+
sendsVars && exists
|
|
129
|
+
? tm1Client.processes.getVariableLayout(processName)
|
|
130
|
+
: Promise.resolve({ variables: [] }),
|
|
131
|
+
dataSource !== undefined && exists
|
|
132
|
+
? tm1Client.processes.getDataSource(processName)
|
|
133
|
+
: Promise.resolve({}),
|
|
134
|
+
]);
|
|
135
|
+
const vars = sendsVars
|
|
136
|
+
? diffVars(installedVars.variables, variables)
|
|
137
|
+
: undefined;
|
|
138
|
+
const ds = dataSource !== undefined
|
|
139
|
+
? // updateDataSource PATCHes only the fields sent; the rest stay.
|
|
140
|
+
diffDs(installedDs, {
|
|
141
|
+
...installedDs,
|
|
142
|
+
...dataSource,
|
|
143
|
+
})
|
|
144
|
+
: undefined;
|
|
145
|
+
const identical = Object.values(tabs).every((t) => t.identical) &&
|
|
146
|
+
params.identical &&
|
|
147
|
+
(vars?.identical ?? true) &&
|
|
148
|
+
(ds?.identical ?? true);
|
|
149
|
+
return {
|
|
150
|
+
content: [
|
|
151
|
+
{
|
|
152
|
+
type: "text",
|
|
153
|
+
text: JSON.stringify({
|
|
154
|
+
processName,
|
|
155
|
+
dryRun: true,
|
|
156
|
+
action: exists ? "wouldUpdate" : "wouldCreate",
|
|
157
|
+
appliedSteps: [],
|
|
158
|
+
overwritesExisting: exists,
|
|
159
|
+
checks,
|
|
160
|
+
diff: {
|
|
161
|
+
identical,
|
|
162
|
+
tabs,
|
|
163
|
+
parameters: params,
|
|
164
|
+
...(vars ? { variables: vars } : {}),
|
|
165
|
+
...(ds ? { dataSource: ds } : {}),
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// Replacing an installed process is not undoable through the API.
|
|
173
|
+
if (exists)
|
|
174
|
+
requireOverwriteConfirm(confirm, processName, "process");
|
|
175
|
+
if (preflight) {
|
|
176
|
+
const failure = await runPreflight(tm1Client, await resolve());
|
|
100
177
|
if (failure)
|
|
101
178
|
return preflightResult(failure);
|
|
102
179
|
}
|
|
@@ -137,6 +214,17 @@ export const registerUpsertProcess = defineTool({
|
|
|
137
214
|
// Process body/parameters/datasource may have changed call sites — drop the
|
|
138
215
|
// 60s callgraph TTL so the next analysis sees fresh references instead of stale graph.
|
|
139
216
|
const { cleared: callgraphEntriesCleared } = invalidateCallgraphCache();
|
|
217
|
+
// Read the code back: TM1 stores what REST sent, so a mismatch means the
|
|
218
|
+
// write did not land as sent. Saves the caller a get_process_code turn.
|
|
219
|
+
const sent = { prolog, metadata, data, epilog };
|
|
220
|
+
const sentTabs = TABS.filter((t) => sent[t] !== undefined);
|
|
221
|
+
let verified;
|
|
222
|
+
if (sentTabs.length > 0) {
|
|
223
|
+
const stored = await tm1Client.processes.getCode(processName);
|
|
224
|
+
const norm = (x) => (x ?? "").replace(/\r\n/g, "\n").trimEnd();
|
|
225
|
+
const mismatchedTabs = sentTabs.filter((t) => norm(stored[t]) !== norm(sent[t]));
|
|
226
|
+
verified = { codeMatches: mismatchedTabs.length === 0, mismatchedTabs };
|
|
227
|
+
}
|
|
140
228
|
let compile;
|
|
141
229
|
if (autoCompile) {
|
|
142
230
|
const result = await tm1Client.processes.compile(processName);
|
|
@@ -157,6 +245,7 @@ export const registerUpsertProcess = defineTool({
|
|
|
157
245
|
appliedSteps: trail,
|
|
158
246
|
callgraphEntriesCleared,
|
|
159
247
|
...(backup ? { backup } : {}),
|
|
248
|
+
...(verified !== undefined ? { verified } : {}),
|
|
160
249
|
...(compile !== undefined ? { compile } : {}),
|
|
161
250
|
}, null, 2),
|
|
162
251
|
},
|