@tiwater/office-mcp 0.3.0 → 0.5.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/_shared/tool-runtime.mjs +34 -1
- package/office/README.md +1 -0
- package/office/index.mjs +183 -22
- package/package.json +1 -1
package/_shared/tool-runtime.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import { constants as fsConstants } from 'node:fs';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -78,7 +79,7 @@ export function requireString(value, label) {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
async function runCommand(candidate, args, options) {
|
|
81
|
-
const env = { ...process.env, ...(candidate.env || {}), ...(options.env || {}) };
|
|
82
|
+
const env = await withDotnetRoot({ ...process.env, ...(candidate.env || {}), ...(options.env || {}) });
|
|
82
83
|
const cwd = candidate.cwd || options.cwd || repoRoot;
|
|
83
84
|
const commandArgs = [...(candidate.argsPrefix || []), ...args];
|
|
84
85
|
|
|
@@ -106,3 +107,35 @@ async function runCommand(candidate, args, options) {
|
|
|
106
107
|
});
|
|
107
108
|
});
|
|
108
109
|
}
|
|
110
|
+
|
|
111
|
+
async function withDotnetRoot(env) {
|
|
112
|
+
const architectureVariable = process.arch === 'arm64'
|
|
113
|
+
? 'DOTNET_ROOT_ARM64'
|
|
114
|
+
: process.arch === 'x64'
|
|
115
|
+
? 'DOTNET_ROOT_X64'
|
|
116
|
+
: null;
|
|
117
|
+
if (env.DOTNET_ROOT || (architectureVariable && env[architectureVariable])) return env;
|
|
118
|
+
|
|
119
|
+
const dotnet = await findOnPath(process.platform === 'win32' ? 'dotnet.exe' : 'dotnet', env.PATH);
|
|
120
|
+
if (!dotnet) return env;
|
|
121
|
+
|
|
122
|
+
const root = path.dirname(dotnet);
|
|
123
|
+
return {
|
|
124
|
+
...env,
|
|
125
|
+
DOTNET_ROOT: root,
|
|
126
|
+
...(architectureVariable ? { [architectureVariable]: root } : {}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function findOnPath(command, pathValue) {
|
|
131
|
+
for (const directory of String(pathValue ?? '').split(path.delimiter).filter(Boolean)) {
|
|
132
|
+
const candidate = path.join(directory, command);
|
|
133
|
+
try {
|
|
134
|
+
await fs.access(candidate, fsConstants.X_OK);
|
|
135
|
+
return candidate;
|
|
136
|
+
} catch {
|
|
137
|
+
// Continue to the next PATH entry.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
package/office/README.md
CHANGED
package/office/index.mjs
CHANGED
|
@@ -72,6 +72,7 @@ const templateMigrationInput = z.object({
|
|
|
72
72
|
source: pathInput.describe('Path to the current source DOCX.'),
|
|
73
73
|
baseline: pathInput.describe('Path to the selected current baseline DOCX.'),
|
|
74
74
|
output: pathInput.describe('Path to the migrated output DOCX.'),
|
|
75
|
+
receiptOutput: pathInput.describe('New JSON receipt artifact path. Existing files are never overwritten.'),
|
|
75
76
|
choices: z.array(migrationChoiceInput).describe('Exactly one business choice for every source id returned by docx_list_migration_choices.'),
|
|
76
77
|
templateCleanup: z.array(templateCleanupInput).optional().describe('Optional baseline-owned placeholders or example rows to clear.'),
|
|
77
78
|
}).strict();
|
|
@@ -92,24 +93,103 @@ const migrationChoiceOutput = z.object({
|
|
|
92
93
|
allowedActions: z.array(z.string()),
|
|
93
94
|
}).strict();
|
|
94
95
|
|
|
96
|
+
const migrationCatalog = z.object({
|
|
97
|
+
schema: z.string(),
|
|
98
|
+
pass: z.boolean(),
|
|
99
|
+
sourceSha256: z.string(),
|
|
100
|
+
baselineSha256: z.string(),
|
|
101
|
+
sources: z.array(migrationChoiceOutput),
|
|
102
|
+
targets: z.array(migrationChoiceOutput),
|
|
103
|
+
}).strict().superRefine((catalog, context) => {
|
|
104
|
+
for (const key of ['sources', 'targets']) {
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
for (const [index, choice] of catalog[key].entries()) {
|
|
107
|
+
if (seen.has(choice.id)) {
|
|
108
|
+
context.addIssue({ code: 'custom', path: [key, index, 'id'], message: `duplicate ${key} choice id: ${choice.id}` });
|
|
109
|
+
}
|
|
110
|
+
seen.add(choice.id);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const migrationReceipt = z.object({
|
|
116
|
+
schema: z.string(),
|
|
117
|
+
toolVersion: z.string(),
|
|
118
|
+
status: z.enum(['pass', 'review-required', 'failed']),
|
|
119
|
+
pass: z.boolean(),
|
|
120
|
+
reviewRequired: z.boolean(),
|
|
121
|
+
outputVerified: z.boolean(),
|
|
122
|
+
output: z.string().nullable(),
|
|
123
|
+
plan: z.string().nullable(),
|
|
124
|
+
failures: z.array(z.unknown()),
|
|
125
|
+
}).passthrough();
|
|
126
|
+
|
|
127
|
+
const inputOnly = z.object({ input: pathInput }).strict();
|
|
128
|
+
const artifactInput = z.object({
|
|
129
|
+
input: pathInput,
|
|
130
|
+
output: pathInput.describe('New JSON artifact path. Existing files are never overwritten.'),
|
|
131
|
+
}).strict();
|
|
132
|
+
const artifact = z.object({
|
|
133
|
+
path: z.string(),
|
|
134
|
+
sha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
135
|
+
bytes: z.number().int().nonnegative(),
|
|
136
|
+
}).strict();
|
|
137
|
+
|
|
95
138
|
const migrationCatalogOutput = z.object({
|
|
96
139
|
tool: z.literal('docx_list_migration_choices'),
|
|
97
140
|
runtime: runtimeIdentity,
|
|
98
|
-
|
|
141
|
+
artifact,
|
|
142
|
+
summary: z.object({
|
|
99
143
|
schema: z.string(),
|
|
100
144
|
pass: z.boolean(),
|
|
101
145
|
sourceSha256: z.string(),
|
|
102
146
|
baselineSha256: z.string(),
|
|
103
|
-
|
|
104
|
-
|
|
147
|
+
sourceCount: z.number().int().nonnegative(),
|
|
148
|
+
targetCount: z.number().int().nonnegative(),
|
|
105
149
|
}).strict(),
|
|
106
150
|
}).strict();
|
|
107
151
|
|
|
152
|
+
const migrationQueryPage = z.object({
|
|
153
|
+
offset: z.number().int().nonnegative(),
|
|
154
|
+
returned: z.number().int().nonnegative(),
|
|
155
|
+
total: z.number().int().nonnegative(),
|
|
156
|
+
hasMore: z.boolean(),
|
|
157
|
+
}).strict();
|
|
158
|
+
|
|
159
|
+
const migrationChoiceQueryInput = z.discriminatedUnion('view', [
|
|
160
|
+
z.object({
|
|
161
|
+
catalog: pathInput.describe('Path returned by docx_list_migration_choices.'),
|
|
162
|
+
view: z.literal('sources'),
|
|
163
|
+
offset: z.number().int().nonnegative().optional(),
|
|
164
|
+
limit: z.number().int().min(1).max(10).optional(),
|
|
165
|
+
}).strict(),
|
|
166
|
+
z.object({
|
|
167
|
+
catalog: pathInput.describe('Path returned by docx_list_migration_choices.'),
|
|
168
|
+
view: z.literal('targets'),
|
|
169
|
+
sourceChoiceId: z.string().trim().min(1),
|
|
170
|
+
text: z.string().trim().min(1).optional().describe('Literal case-insensitive text to find in target text or visible context.'),
|
|
171
|
+
kinds: z.array(z.string().trim().min(1)).min(1).optional(),
|
|
172
|
+
scopes: z.array(z.string().trim().min(1)).min(1).optional(),
|
|
173
|
+
offset: z.number().int().nonnegative().optional(),
|
|
174
|
+
limit: z.number().int().min(1).max(10).optional(),
|
|
175
|
+
}).strict(),
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
const migrationChoiceQueryOutput = z.object({
|
|
179
|
+
tool: z.literal('docx_query_migration_choices'),
|
|
180
|
+
catalogSha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
181
|
+
view: z.enum(['sources', 'targets']),
|
|
182
|
+
source: migrationChoiceOutput.nullable(),
|
|
183
|
+
items: z.array(migrationChoiceOutput),
|
|
184
|
+
page: migrationQueryPage,
|
|
185
|
+
}).strict();
|
|
186
|
+
|
|
108
187
|
function migrationReceiptOutput(tool) {
|
|
109
188
|
return z.object({
|
|
110
189
|
tool: z.literal(tool),
|
|
111
190
|
runtime: runtimeIdentity,
|
|
112
|
-
|
|
191
|
+
artifact,
|
|
192
|
+
summary: z.object({
|
|
113
193
|
schema: z.string(),
|
|
114
194
|
toolVersion: z.string(),
|
|
115
195
|
status: z.enum(['pass', 'review-required', 'failed']),
|
|
@@ -118,22 +198,11 @@ function migrationReceiptOutput(tool) {
|
|
|
118
198
|
outputVerified: z.boolean(),
|
|
119
199
|
output: z.string().nullable(),
|
|
120
200
|
plan: z.string().nullable(),
|
|
121
|
-
|
|
122
|
-
}).
|
|
201
|
+
failureCount: z.number().int().nonnegative(),
|
|
202
|
+
}).strict(),
|
|
123
203
|
}).strict();
|
|
124
204
|
}
|
|
125
205
|
|
|
126
|
-
const inputOnly = z.object({ input: pathInput }).strict();
|
|
127
|
-
const artifactInput = z.object({
|
|
128
|
-
input: pathInput,
|
|
129
|
-
output: pathInput.describe('New JSON artifact path. Existing files are never overwritten.'),
|
|
130
|
-
}).strict();
|
|
131
|
-
const artifact = z.object({
|
|
132
|
-
path: z.string(),
|
|
133
|
-
sha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
134
|
-
bytes: z.number().int().nonnegative(),
|
|
135
|
-
}).strict();
|
|
136
|
-
|
|
137
206
|
function artifactOutput(tool) {
|
|
138
207
|
return z.object({ tool: z.literal(tool), runtime: runtimeIdentity, artifact }).strict();
|
|
139
208
|
}
|
|
@@ -148,15 +217,23 @@ const tools = [
|
|
|
148
217
|
},
|
|
149
218
|
{
|
|
150
219
|
name: 'docx_list_migration_choices',
|
|
151
|
-
description: '
|
|
220
|
+
description: 'Write every current source item that still needs a business choice and the selectable current baseline targets to a run-local JSON artifact. Returns only artifact metadata and counts; it does not recommend a choice.',
|
|
152
221
|
inputSchema: z.object({
|
|
153
222
|
source: pathInput.describe('Path to the current source DOCX.'),
|
|
154
223
|
baseline: pathInput.describe('Path to the selected current baseline DOCX.'),
|
|
224
|
+
output: pathInput.describe('New JSON artifact path. Existing files are never overwritten.'),
|
|
155
225
|
}).strict(),
|
|
156
226
|
outputSchema: migrationCatalogOutput,
|
|
157
|
-
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
158
227
|
handler: docxListMigrationChoices,
|
|
159
228
|
},
|
|
229
|
+
{
|
|
230
|
+
name: 'docx_query_migration_choices',
|
|
231
|
+
description: 'Read one bounded page from a migration-choice catalog. List source choices, or inspect targets for one source using literal text, kind, and scope filters. This tool does not recommend or make a business choice.',
|
|
232
|
+
inputSchema: migrationChoiceQueryInput,
|
|
233
|
+
outputSchema: migrationChoiceQueryOutput,
|
|
234
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
235
|
+
handler: docxQueryMigrationChoices,
|
|
236
|
+
},
|
|
160
237
|
{
|
|
161
238
|
name: 'docx_migrate_template',
|
|
162
239
|
description: 'Migrate a current DOCX into the selected baseline from one complete batch of business choices. Choices reference only opaque ids returned by docx_list_migration_choices; the tool derives all document values, coordinates, plans, and edits.',
|
|
@@ -169,7 +246,6 @@ const tools = [
|
|
|
169
246
|
description: 'Independently re-resolve the same business choices and verify a migrated DOCX against the current source and baseline. This does not trust the migration receipt.',
|
|
170
247
|
inputSchema: templateMigrationInput,
|
|
171
248
|
outputSchema: migrationReceiptOutput('docx_verify_migration'),
|
|
172
|
-
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
173
249
|
handler: docxVerifyMigration,
|
|
174
250
|
},
|
|
175
251
|
{
|
|
@@ -270,7 +346,76 @@ async function docxListMigrationChoices(args) {
|
|
|
270
346
|
const source = requireString(args.source, 'source');
|
|
271
347
|
const baseline = requireString(args.baseline, 'baseline');
|
|
272
348
|
const result = await runJsonCandidateChain(docxCandidates, ['list-template-migration-choices', source, baseline]);
|
|
273
|
-
|
|
349
|
+
const catalog = migrationCatalog.parse(result.json);
|
|
350
|
+
return {
|
|
351
|
+
tool: 'docx_list_migration_choices',
|
|
352
|
+
runtime: commandRuntime(result),
|
|
353
|
+
artifact: await writeJsonArtifact(requireString(args.output, 'output'), catalog),
|
|
354
|
+
summary: {
|
|
355
|
+
schema: catalog.schema,
|
|
356
|
+
pass: catalog.pass,
|
|
357
|
+
sourceSha256: catalog.sourceSha256,
|
|
358
|
+
baselineSha256: catalog.baselineSha256,
|
|
359
|
+
sourceCount: catalog.sources.length,
|
|
360
|
+
targetCount: catalog.targets.length,
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function docxQueryMigrationChoices(args) {
|
|
366
|
+
const catalogPath = requireString(args.catalog, 'catalog');
|
|
367
|
+
const bytes = await readFile(catalogPath);
|
|
368
|
+
const catalog = migrationCatalog.parse(JSON.parse(bytes.toString('utf8')));
|
|
369
|
+
const offset = args.offset ?? 0;
|
|
370
|
+
const limit = args.limit ?? 10;
|
|
371
|
+
let source = null;
|
|
372
|
+
let matches;
|
|
373
|
+
|
|
374
|
+
if (args.view === 'sources') {
|
|
375
|
+
matches = catalog.sources;
|
|
376
|
+
} else {
|
|
377
|
+
source = catalog.sources.find(item => item.id === args.sourceChoiceId) ?? null;
|
|
378
|
+
if (!source) {
|
|
379
|
+
throw Object.assign(new Error(`Unknown sourceChoiceId: ${args.sourceChoiceId}`), { code: -32602 });
|
|
380
|
+
}
|
|
381
|
+
const kinds = args.kinds ? new Set(args.kinds) : null;
|
|
382
|
+
const scopes = args.scopes ? new Set(args.scopes) : null;
|
|
383
|
+
const textQuery = args.text?.toLocaleLowerCase();
|
|
384
|
+
matches = catalog.targets.filter(item =>
|
|
385
|
+
(!kinds || kinds.has(item.kind)) &&
|
|
386
|
+
(!scopes || scopes.has(item.scope)) &&
|
|
387
|
+
(!textQuery || migrationChoiceSearchText(item).includes(textQuery)));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const items = matches.slice(offset, offset + limit);
|
|
391
|
+
return {
|
|
392
|
+
tool: 'docx_query_migration_choices',
|
|
393
|
+
catalogSha256: createHash('sha256').update(bytes).digest('hex'),
|
|
394
|
+
view: args.view,
|
|
395
|
+
source,
|
|
396
|
+
items,
|
|
397
|
+
page: {
|
|
398
|
+
offset,
|
|
399
|
+
returned: items.length,
|
|
400
|
+
total: matches.length,
|
|
401
|
+
hasMore: offset + items.length < matches.length,
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function migrationChoiceSearchText(choice) {
|
|
407
|
+
return collectVisibleStrings({ text: choice.text, context: choice.context })
|
|
408
|
+
.join('\n')
|
|
409
|
+
.toLocaleLowerCase();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function collectVisibleStrings(value, fieldName = '') {
|
|
413
|
+
if (typeof value === 'string') return /text/i.test(fieldName) ? [value] : [];
|
|
414
|
+
if (Array.isArray(value)) return value.flatMap(item => collectVisibleStrings(item, fieldName));
|
|
415
|
+
if (value && typeof value === 'object') {
|
|
416
|
+
return Object.entries(value).flatMap(([key, item]) => collectVisibleStrings(item, key));
|
|
417
|
+
}
|
|
418
|
+
return [];
|
|
274
419
|
}
|
|
275
420
|
|
|
276
421
|
async function docxMigrateTemplate(args) {
|
|
@@ -298,7 +443,23 @@ async function runTemplateMigrationCommand(tool, command, args) {
|
|
|
298
443
|
docxCandidates,
|
|
299
444
|
[command, source, baseline, choicesPath, output],
|
|
300
445
|
{ allowedExitCodes: [0, 1] });
|
|
301
|
-
|
|
446
|
+
const receipt = migrationReceipt.parse(result.json);
|
|
447
|
+
return {
|
|
448
|
+
tool,
|
|
449
|
+
runtime: commandRuntime(result),
|
|
450
|
+
artifact: await writeJsonArtifact(requireString(args.receiptOutput, 'receiptOutput'), receipt),
|
|
451
|
+
summary: {
|
|
452
|
+
schema: receipt.schema,
|
|
453
|
+
toolVersion: receipt.toolVersion,
|
|
454
|
+
status: receipt.status,
|
|
455
|
+
pass: receipt.pass,
|
|
456
|
+
reviewRequired: receipt.reviewRequired,
|
|
457
|
+
outputVerified: receipt.outputVerified,
|
|
458
|
+
output: receipt.output,
|
|
459
|
+
plan: receipt.plan,
|
|
460
|
+
failureCount: receipt.failures.length,
|
|
461
|
+
},
|
|
462
|
+
};
|
|
302
463
|
});
|
|
303
464
|
}
|
|
304
465
|
|