@tiangong-ai/cli 0.0.48 → 0.0.50
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/AGENTS.md +2 -2
- package/README.md +48 -1
- package/dist/research/setup-command.js +52 -0
- package/dist/research/setup-command.js.map +1 -1
- package/dist/research/workspace/setup-audit-bundle.d.ts +69 -0
- package/dist/research/workspace/setup-audit-bundle.js +1695 -0
- package/dist/research/workspace/setup-audit-bundle.js.map +1 -0
- package/dist/research/workspace/setup-catalog.d.ts +1 -1
- package/dist/research/workspace/setup-catalog.js +193 -6
- package/dist/research/workspace/setup-catalog.js.map +1 -1
- package/dist/research/workspace/setup.js +467 -17
- package/dist/research/workspace/setup.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { chmod, link, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, } from "node:fs/promises";
|
|
4
|
-
import { hostname, homedir, platform } from "node:os";
|
|
5
|
-
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { chmod, link, lstat, mkdtemp, mkdir, open, readFile, readdir, realpath, rename, rm, } from "node:fs/promises";
|
|
4
|
+
import { hostname, homedir, platform, tmpdir } from "node:os";
|
|
5
|
+
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
7
7
|
import { CliError } from "../../errors.js";
|
|
8
8
|
import { loadCapabilityDeclarations } from "./capabilities.js";
|
|
@@ -1033,7 +1033,7 @@ async function containsUnmanagedPathCliFallback(root) {
|
|
|
1033
1033
|
};
|
|
1034
1034
|
return inspectDirectory(root, 0);
|
|
1035
1035
|
}
|
|
1036
|
-
async function findAmbientExecutable(environment, executable) {
|
|
1036
|
+
async function findAmbientExecutable(environment, executable, options = {}) {
|
|
1037
1037
|
const pathValue = environment.PATH;
|
|
1038
1038
|
if (!pathValue)
|
|
1039
1039
|
return null;
|
|
@@ -1042,7 +1042,7 @@ async function findAmbientExecutable(environment, executable) {
|
|
|
1042
1042
|
if (!directory)
|
|
1043
1043
|
continue;
|
|
1044
1044
|
for (const suffix of suffixes) {
|
|
1045
|
-
const candidate =
|
|
1045
|
+
const candidate = resolve(directory, `${executable}${suffix}`);
|
|
1046
1046
|
const info = await lstat(candidate).catch(() => undefined);
|
|
1047
1047
|
if (!info || (!info.isFile() && !info.isSymbolicLink()))
|
|
1048
1048
|
continue;
|
|
@@ -1050,8 +1050,12 @@ async function findAmbientExecutable(environment, executable) {
|
|
|
1050
1050
|
if (!resolved)
|
|
1051
1051
|
continue;
|
|
1052
1052
|
const resolvedInfo = await lstat(resolved).catch(() => undefined);
|
|
1053
|
-
if (resolvedInfo?.isFile() && !resolvedInfo.isSymbolicLink())
|
|
1054
|
-
|
|
1053
|
+
if (resolvedInfo?.isFile() && !resolvedInfo.isSymbolicLink()) {
|
|
1054
|
+
const path = options.preserveLeafSymlink
|
|
1055
|
+
? join(await realpath(dirname(candidate)), basename(candidate))
|
|
1056
|
+
: resolved;
|
|
1057
|
+
return { path, ignoredByExactInvocation: true };
|
|
1058
|
+
}
|
|
1055
1059
|
}
|
|
1056
1060
|
}
|
|
1057
1061
|
return null;
|
|
@@ -1446,7 +1450,16 @@ export async function doctorResearchSetup(workspace, options = {}) {
|
|
|
1446
1450
|
: `Run research setup credential set --id ${credential.id} --prompt --workspace ${root}.`,
|
|
1447
1451
|
});
|
|
1448
1452
|
}
|
|
1449
|
-
await
|
|
1453
|
+
const authoringRuntime = await resolveAuthoringRuntime(selected, environment);
|
|
1454
|
+
await appendDependencyChecks(checks, plan, selected, runner, root, environment, authoringRuntime);
|
|
1455
|
+
await appendAuthoringCanaryChecks(checks, {
|
|
1456
|
+
plan,
|
|
1457
|
+
selected,
|
|
1458
|
+
runner,
|
|
1459
|
+
root,
|
|
1460
|
+
environment,
|
|
1461
|
+
runtime: authoringRuntime,
|
|
1462
|
+
});
|
|
1450
1463
|
let capabilityDoctor = null;
|
|
1451
1464
|
const blockingBeforeLive = checks
|
|
1452
1465
|
.map((check) => normalizeSetupDoctorCheck(check, selected))
|
|
@@ -2926,10 +2939,10 @@ async function runAcademicPaperCompanion(input) {
|
|
|
2926
2939
|
exitCode: 2,
|
|
2927
2940
|
});
|
|
2928
2941
|
}
|
|
2929
|
-
const
|
|
2930
|
-
await requireRegularCompanionFile(input.root,
|
|
2942
|
+
const runtimeScript = join(input.skillDirectory, "scripts", "runtime.py");
|
|
2943
|
+
await requireRegularCompanionFile(input.root, runtimeScript, "locked runtime script");
|
|
2931
2944
|
const timeoutSeconds = normalizedCompanionTimeout(input.input.timeoutSeconds, 30, 600);
|
|
2932
|
-
const args = [
|
|
2945
|
+
const args = [runtimeScript, "fetch"];
|
|
2933
2946
|
if (doi)
|
|
2934
2947
|
args.push(doi);
|
|
2935
2948
|
else
|
|
@@ -2954,6 +2967,18 @@ async function runAcademicPaperCompanion(input) {
|
|
|
2954
2967
|
timeoutMs: (timeoutSeconds + 60) * 4 * 1_000,
|
|
2955
2968
|
});
|
|
2956
2969
|
const envelope = parseCompanionJson(execution.stdout, input.root, "academic-paper-download");
|
|
2970
|
+
if (execution.exitCode !== 0 && envelope.ok === false && isObject(envelope.error)) {
|
|
2971
|
+
const runtimeErrorCode = safeAcademicPaperRuntimeErrorCode(envelope.error.code);
|
|
2972
|
+
throw setupError({
|
|
2973
|
+
code: "RESEARCH_SETUP_COMPANION_RUNTIME_INVALID",
|
|
2974
|
+
step: "companion-runtime",
|
|
2975
|
+
reason: `The academic paper locked runtime failed preflight (${runtimeErrorCode}).`,
|
|
2976
|
+
minimumAction: "From the verified installed academic-paper-download Skill directory, run `python3 scripts/runtime.py bootstrap --locked --json`, review the result, then rerun setup doctor and the companion command.",
|
|
2977
|
+
retryCommand: `tiangong-ai research setup doctor --workspace ${input.root} --json`,
|
|
2978
|
+
exitCode: 3,
|
|
2979
|
+
diagnostics: { runtimeErrorCode },
|
|
2980
|
+
});
|
|
2981
|
+
}
|
|
2957
2982
|
const results = isObject(envelope.data) && Array.isArray(envelope.data.results) ? envelope.data.results : [];
|
|
2958
2983
|
if (results.length !== 1 || !isObject(results[0])) {
|
|
2959
2984
|
throw setupError({
|
|
@@ -3073,6 +3098,18 @@ async function runAcademicPaperCompanion(input) {
|
|
|
3073
3098
|
next: "Admit the exact PDF or a derived hash-bound view as a declared research input.",
|
|
3074
3099
|
};
|
|
3075
3100
|
}
|
|
3101
|
+
function safeAcademicPaperRuntimeErrorCode(value) {
|
|
3102
|
+
const allowed = new Set([
|
|
3103
|
+
"python_incompatible",
|
|
3104
|
+
"lock_missing",
|
|
3105
|
+
"lock_invalid",
|
|
3106
|
+
"runtime_missing",
|
|
3107
|
+
"runtime_invalid",
|
|
3108
|
+
"runtime_io_error",
|
|
3109
|
+
"dependency_install_failed",
|
|
3110
|
+
]);
|
|
3111
|
+
return typeof value === "string" && allowed.has(value) ? value : "runtime_error";
|
|
3112
|
+
}
|
|
3076
3113
|
function companionEnvironment(source) {
|
|
3077
3114
|
const result = installerEnvironment(source);
|
|
3078
3115
|
delete result.CI;
|
|
@@ -3475,6 +3512,10 @@ function setupDomainReadiness(checks, scope) {
|
|
|
3475
3512
|
return "NOT_REQUIRED";
|
|
3476
3513
|
if (matching.some((check) => check.blocking && check.status === "fail"))
|
|
3477
3514
|
return "BLOCKED";
|
|
3515
|
+
if (scope === "authoring" &&
|
|
3516
|
+
matching.some((check) => check.componentGate && check.status === "fail")) {
|
|
3517
|
+
return "BLOCKED";
|
|
3518
|
+
}
|
|
3478
3519
|
return matching.some((check) => check.status !== "pass") ? "DEGRADED" : "READY";
|
|
3479
3520
|
}
|
|
3480
3521
|
function requireAbsoluteWorkspace(value) {
|
|
@@ -3725,6 +3766,7 @@ function installerEnvironment(source) {
|
|
|
3725
3766
|
"SSL_CERT_FILE",
|
|
3726
3767
|
"SSL_CERT_DIR",
|
|
3727
3768
|
"NODE_EXTRA_CA_CERTS",
|
|
3769
|
+
"NODE_PATH",
|
|
3728
3770
|
"CODEX_HOME",
|
|
3729
3771
|
"CLAUDE_CONFIG_DIR",
|
|
3730
3772
|
"VIRTUAL_ENV",
|
|
@@ -3893,7 +3935,99 @@ async function setupDoctorCheck(checks, id, category, callback, failureMinimumAc
|
|
|
3893
3935
|
});
|
|
3894
3936
|
}
|
|
3895
3937
|
}
|
|
3896
|
-
|
|
3938
|
+
const AUTHORING_PYTHON_PROBES = {
|
|
3939
|
+
"authoring:defusedxml": {
|
|
3940
|
+
distribution: "defusedxml",
|
|
3941
|
+
module: "defusedxml",
|
|
3942
|
+
exactVersion: "0.7.1",
|
|
3943
|
+
},
|
|
3944
|
+
"authoring:lxml": { distribution: "lxml", module: "lxml.etree", exactVersion: null },
|
|
3945
|
+
"authoring:pillow": { distribution: "Pillow", module: "PIL", exactVersion: null },
|
|
3946
|
+
"authoring:python-pptx": {
|
|
3947
|
+
distribution: "python-pptx",
|
|
3948
|
+
module: "pptx",
|
|
3949
|
+
exactVersion: null,
|
|
3950
|
+
},
|
|
3951
|
+
"authoring:markitdown": {
|
|
3952
|
+
distribution: "markitdown",
|
|
3953
|
+
module: "markitdown",
|
|
3954
|
+
exactVersion: "0.1.7",
|
|
3955
|
+
},
|
|
3956
|
+
"authoring:pypdf": { distribution: "pypdf", module: "pypdf", exactVersion: null },
|
|
3957
|
+
"authoring:pdfplumber": {
|
|
3958
|
+
distribution: "pdfplumber",
|
|
3959
|
+
module: "pdfplumber",
|
|
3960
|
+
exactVersion: null,
|
|
3961
|
+
},
|
|
3962
|
+
"authoring:reportlab": {
|
|
3963
|
+
distribution: "reportlab",
|
|
3964
|
+
module: "reportlab",
|
|
3965
|
+
exactVersion: null,
|
|
3966
|
+
},
|
|
3967
|
+
"authoring:pdf2image": {
|
|
3968
|
+
distribution: "pdf2image",
|
|
3969
|
+
module: "pdf2image",
|
|
3970
|
+
exactVersion: null,
|
|
3971
|
+
},
|
|
3972
|
+
"authoring:openpyxl": {
|
|
3973
|
+
distribution: "openpyxl",
|
|
3974
|
+
module: "openpyxl",
|
|
3975
|
+
exactVersion: null,
|
|
3976
|
+
},
|
|
3977
|
+
"authoring:pandas": { distribution: "pandas", module: "pandas", exactVersion: null },
|
|
3978
|
+
};
|
|
3979
|
+
const AUTHORING_NODE_PROBES = {
|
|
3980
|
+
"authoring:node-docx": "docx",
|
|
3981
|
+
"authoring:node-pptxgenjs": "pptxgenjs",
|
|
3982
|
+
};
|
|
3983
|
+
const AUTHORING_COMMAND_PROBES = {
|
|
3984
|
+
"authoring:pandoc": "pandoc",
|
|
3985
|
+
"authoring:libreoffice": "soffice",
|
|
3986
|
+
"authoring:poppler": "pdftoppm",
|
|
3987
|
+
"authoring:zip": "zip",
|
|
3988
|
+
"authoring:unzip": "unzip",
|
|
3989
|
+
};
|
|
3990
|
+
async function resolveAuthoringRuntime(selected, environment) {
|
|
3991
|
+
if (!selected.some((skill) => skill.role === "post-closure-authoring"))
|
|
3992
|
+
return null;
|
|
3993
|
+
const resolveCommand = async (name) => (await findAmbientExecutable(environment, name, {
|
|
3994
|
+
preserveLeafSymlink: name === "python3",
|
|
3995
|
+
}))?.path ?? null;
|
|
3996
|
+
const [python, node, pandoc, soffice, pdftoppm, zip, unzip] = await Promise.all([
|
|
3997
|
+
resolveCommand("python3"),
|
|
3998
|
+
resolveCommand("node"),
|
|
3999
|
+
resolveCommand("pandoc"),
|
|
4000
|
+
resolveCommand("soffice"),
|
|
4001
|
+
resolveCommand("pdftoppm"),
|
|
4002
|
+
resolveCommand("zip"),
|
|
4003
|
+
resolveCommand("unzip"),
|
|
4004
|
+
]);
|
|
4005
|
+
return { python, node, pandoc, soffice, pdftoppm, zip, unzip };
|
|
4006
|
+
}
|
|
4007
|
+
function requireAuthoringRuntimeExecutable(value, minimumAction) {
|
|
4008
|
+
if (!value)
|
|
4009
|
+
throw new Error(minimumAction);
|
|
4010
|
+
return value;
|
|
4011
|
+
}
|
|
4012
|
+
function authoringEnvironment(source, runtime) {
|
|
4013
|
+
const result = installerEnvironment(source);
|
|
4014
|
+
const commandDirectories = [
|
|
4015
|
+
runtime?.soffice,
|
|
4016
|
+
runtime?.pdftoppm,
|
|
4017
|
+
runtime?.pandoc,
|
|
4018
|
+
runtime?.zip,
|
|
4019
|
+
runtime?.unzip,
|
|
4020
|
+
]
|
|
4021
|
+
.filter((value) => Boolean(value))
|
|
4022
|
+
.map(dirname);
|
|
4023
|
+
if (commandDirectories.length) {
|
|
4024
|
+
result.PATH = [...new Set(commandDirectories), result.PATH ?? ""]
|
|
4025
|
+
.filter(Boolean)
|
|
4026
|
+
.join(delimiter);
|
|
4027
|
+
}
|
|
4028
|
+
return result;
|
|
4029
|
+
}
|
|
4030
|
+
async function appendDependencyChecks(checks, plan, selected, runner, root, environment, authoringRuntime) {
|
|
3897
4031
|
const dependencies = [
|
|
3898
4032
|
...new Map(selected
|
|
3899
4033
|
.flatMap((skill) => skill.dependencies)
|
|
@@ -3913,7 +4047,7 @@ async function appendDependencyChecks(checks, selected, runner, root, environmen
|
|
|
3913
4047
|
await setupDoctorCheck(checks, `dependency.${dependency.id}`, "dependency", async () => {
|
|
3914
4048
|
if (dependency.id === "python-3.10") {
|
|
3915
4049
|
const result = await runner({
|
|
3916
|
-
command: "python3",
|
|
4050
|
+
command: authoringRuntime?.python ?? "python3",
|
|
3917
4051
|
args: ["--version"],
|
|
3918
4052
|
cwd: root,
|
|
3919
4053
|
environment: installerEnvironment(environment),
|
|
@@ -3929,23 +4063,339 @@ async function appendDependencyChecks(checks, selected, runner, root, environmen
|
|
|
3929
4063
|
return `${versionText.trim()} satisfies ${dependency.requirement}.`;
|
|
3930
4064
|
}
|
|
3931
4065
|
if (dependency.id === "academic-paper-download:pypdf") {
|
|
4066
|
+
const skill = setupSkill("tiangong.academic-paper-download");
|
|
4067
|
+
const skillDirectory = await verifiedCompanionSkillDirectory(plan, skill);
|
|
4068
|
+
const runtimeScript = join(skillDirectory, "scripts", "runtime.py");
|
|
4069
|
+
await requireRegularCompanionFile(root, runtimeScript, "locked runtime script");
|
|
3932
4070
|
const result = await runner({
|
|
3933
4071
|
command: "python3",
|
|
3934
|
-
args: ["
|
|
4072
|
+
args: [runtimeScript, "doctor", "--json"],
|
|
4073
|
+
cwd: root,
|
|
4074
|
+
environment: installerEnvironment(environment),
|
|
4075
|
+
timeoutMs: 15_000,
|
|
4076
|
+
});
|
|
4077
|
+
const envelope = parseCompanionJson(result.stdout, root, "academic-paper-download locked runtime doctor");
|
|
4078
|
+
const data = isObject(envelope.data) ? envelope.data : {};
|
|
4079
|
+
if (result.exitCode !== 0 || envelope.ok !== true || data.pypdf !== "6.14.2") {
|
|
4080
|
+
throw new Error("The verified academic paper locked runtime is not ready.");
|
|
4081
|
+
}
|
|
4082
|
+
return `${dependency.requirement} is ready.`;
|
|
4083
|
+
}
|
|
4084
|
+
const pythonProbe = AUTHORING_PYTHON_PROBES[dependency.id];
|
|
4085
|
+
if (pythonProbe) {
|
|
4086
|
+
const python = requireAuthoringRuntimeExecutable(authoringRuntime?.python ?? null, dependency.minimumAction);
|
|
4087
|
+
const result = await runner({
|
|
4088
|
+
command: python,
|
|
4089
|
+
args: [
|
|
4090
|
+
"-c",
|
|
4091
|
+
`import importlib.metadata as m; import ${pythonProbe.module}; print(m.version(${JSON.stringify(pythonProbe.distribution)}))`,
|
|
4092
|
+
],
|
|
3935
4093
|
cwd: root,
|
|
3936
4094
|
environment: installerEnvironment(environment),
|
|
3937
4095
|
timeoutMs: 15_000,
|
|
3938
4096
|
});
|
|
3939
4097
|
const observed = result.stdout.trim();
|
|
3940
|
-
if (result.exitCode !== 0 ||
|
|
3941
|
-
|
|
4098
|
+
if (result.exitCode !== 0 ||
|
|
4099
|
+
!observed ||
|
|
4100
|
+
(pythonProbe.exactVersion !== null && observed !== pythonProbe.exactVersion)) {
|
|
4101
|
+
throw new Error(`${dependency.requirement} is not active in the bound authoring Python.`);
|
|
3942
4102
|
}
|
|
3943
|
-
return `${dependency.requirement} is active.`;
|
|
4103
|
+
return `${dependency.requirement} is active (version ${observed}).`;
|
|
4104
|
+
}
|
|
4105
|
+
const nodeProbe = AUTHORING_NODE_PROBES[dependency.id];
|
|
4106
|
+
if (nodeProbe) {
|
|
4107
|
+
const node = requireAuthoringRuntimeExecutable(authoringRuntime?.node ?? null, dependency.minimumAction);
|
|
4108
|
+
const result = await runner({
|
|
4109
|
+
command: node,
|
|
4110
|
+
args: [
|
|
4111
|
+
"-e",
|
|
4112
|
+
`const resolved=require.resolve(${JSON.stringify(nodeProbe)}); process.stdout.write(resolved);`,
|
|
4113
|
+
],
|
|
4114
|
+
cwd: root,
|
|
4115
|
+
environment: authoringEnvironment(environment, authoringRuntime),
|
|
4116
|
+
timeoutMs: 15_000,
|
|
4117
|
+
});
|
|
4118
|
+
if (result.exitCode !== 0 || !result.stdout.trim()) {
|
|
4119
|
+
throw new Error(`${dependency.requirement} is not resolvable by the bound authoring Node.`);
|
|
4120
|
+
}
|
|
4121
|
+
return `${dependency.requirement} is resolvable.`;
|
|
4122
|
+
}
|
|
4123
|
+
const commandKey = AUTHORING_COMMAND_PROBES[dependency.id];
|
|
4124
|
+
if (commandKey) {
|
|
4125
|
+
const command = requireAuthoringRuntimeExecutable(authoringRuntime?.[commandKey] ?? null, dependency.minimumAction);
|
|
4126
|
+
const result = await runner({
|
|
4127
|
+
command,
|
|
4128
|
+
args: ["pdftoppm", "zip", "unzip"].includes(commandKey) ? ["-v"] : ["--version"],
|
|
4129
|
+
cwd: root,
|
|
4130
|
+
environment: authoringEnvironment(environment, authoringRuntime),
|
|
4131
|
+
timeoutMs: 15_000,
|
|
4132
|
+
});
|
|
4133
|
+
if (result.exitCode !== 0)
|
|
4134
|
+
throw new Error(`${dependency.requirement} is unavailable.`);
|
|
4135
|
+
return `${dependency.requirement} is executable.`;
|
|
3944
4136
|
}
|
|
3945
4137
|
throw new Error(`No automatic dependency check is declared for ${dependency.id}. ${dependency.minimumAction}`);
|
|
3946
4138
|
}, dependency.minimumAction);
|
|
3947
4139
|
}
|
|
3948
4140
|
}
|
|
4141
|
+
async function appendAuthoringCanaryChecks(checks, input) {
|
|
4142
|
+
for (const skill of input.selected.filter((candidate) => candidate.role === "post-closure-authoring" &&
|
|
4143
|
+
["anthropic.docx", "anthropic.pdf", "anthropic.pptx", "anthropic.xlsx"].includes(candidate.id))) {
|
|
4144
|
+
const failedPrerequisites = checks.filter((check) => check.id.startsWith("dependency.") &&
|
|
4145
|
+
check.status === "fail" &&
|
|
4146
|
+
setupCheckComponentIds(check, input.selected).includes(skill.id));
|
|
4147
|
+
if (failedPrerequisites.length) {
|
|
4148
|
+
checks.push({
|
|
4149
|
+
id: `authoring-canary.${skill.id}`,
|
|
4150
|
+
category: "authoring-canary",
|
|
4151
|
+
scope: "authoring",
|
|
4152
|
+
componentIds: [skill.id],
|
|
4153
|
+
status: "fail",
|
|
4154
|
+
detail: `Functional canary was not started because prerequisite checks failed: ${failedPrerequisites.map((check) => check.id).join(", ")}.`,
|
|
4155
|
+
minimumAction: "Resolve every declared runtime and dependency prerequisite, then rerun setup doctor; setup never installs them.",
|
|
4156
|
+
blocking: false,
|
|
4157
|
+
componentGate: true,
|
|
4158
|
+
requiredFor: [`component:${skill.id}`],
|
|
4159
|
+
skippedBecause: failedPrerequisites.map((check) => check.id).join(", "),
|
|
4160
|
+
});
|
|
4161
|
+
continue;
|
|
4162
|
+
}
|
|
4163
|
+
await setupDoctorCheck(checks, `authoring-canary.${skill.id}`, "authoring-canary", async () => {
|
|
4164
|
+
const temporary = await mkdtemp(join(tmpdir(), `tiangong-authoring-${skill.skillName}-`));
|
|
4165
|
+
try {
|
|
4166
|
+
await runAuthoringCanary({ ...input, skill, temporary });
|
|
4167
|
+
return `${skill.id} completed its exact-file synthetic functional canary.`;
|
|
4168
|
+
}
|
|
4169
|
+
finally {
|
|
4170
|
+
await rm(temporary, { recursive: true, force: true });
|
|
4171
|
+
}
|
|
4172
|
+
}, "Inspect the failed pinned helper/package/command step, repair the explicit authoring runtime outside this CLI, and rerun setup doctor.");
|
|
4173
|
+
Object.assign(checks.at(-1), {
|
|
4174
|
+
scope: "authoring",
|
|
4175
|
+
componentIds: [skill.id],
|
|
4176
|
+
blocking: false,
|
|
4177
|
+
componentGate: true,
|
|
4178
|
+
requiredFor: [`component:${skill.id}`],
|
|
4179
|
+
});
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
async function runAuthoringCanary(input) {
|
|
4183
|
+
const environment = authoringEnvironment(input.environment, input.runtime);
|
|
4184
|
+
environment.PYTHONDONTWRITEBYTECODE = "1";
|
|
4185
|
+
if (input.skill.id === "anthropic.docx")
|
|
4186
|
+
return runDocxAuthoringCanary(input, environment);
|
|
4187
|
+
if (input.skill.id === "anthropic.pdf")
|
|
4188
|
+
return runPdfAuthoringCanary(input, environment);
|
|
4189
|
+
if (input.skill.id === "anthropic.pptx")
|
|
4190
|
+
return runPptxAuthoringCanary(input, environment);
|
|
4191
|
+
if (input.skill.id === "anthropic.xlsx")
|
|
4192
|
+
return runXlsxAuthoringCanary(input, environment);
|
|
4193
|
+
throw new Error("Unsupported authoring canary.");
|
|
4194
|
+
}
|
|
4195
|
+
async function runDocxAuthoringCanary(input, environment) {
|
|
4196
|
+
const node = requireAuthoringRuntimeExecutable(input.runtime?.node ?? null, "Node is missing.");
|
|
4197
|
+
const python = requireAuthoringRuntimeExecutable(input.runtime?.python ?? null, "Python is missing.");
|
|
4198
|
+
const pandoc = requireAuthoringRuntimeExecutable(input.runtime?.pandoc ?? null, "Pandoc is missing.");
|
|
4199
|
+
const pdftoppm = requireAuthoringRuntimeExecutable(input.runtime?.pdftoppm ?? null, "Poppler is missing.");
|
|
4200
|
+
const skillDirectory = await verifiedCompanionSkillDirectory(input.plan, input.skill);
|
|
4201
|
+
const validator = join(skillDirectory, "scripts", "office", "validate.py");
|
|
4202
|
+
const soffice = join(skillDirectory, "scripts", "office", "soffice.py");
|
|
4203
|
+
await Promise.all([
|
|
4204
|
+
requireRegularCompanionFile(input.root, validator, "DOCX validator"),
|
|
4205
|
+
requireRegularCompanionFile(input.root, soffice, "DOCX LibreOffice helper"),
|
|
4206
|
+
]);
|
|
4207
|
+
const document = join(input.temporary, "docx-canary.docx");
|
|
4208
|
+
await authoringStep(input, environment, "docx.create", node, [
|
|
4209
|
+
"-e",
|
|
4210
|
+
DOCX_CANARY_SCRIPT,
|
|
4211
|
+
document,
|
|
4212
|
+
]);
|
|
4213
|
+
await requireAuthoringArtifact(document, 128);
|
|
4214
|
+
await authoringStep(input, environment, "docx.validate", python, [validator, document]);
|
|
4215
|
+
const text = await authoringStep(input, environment, "docx.extract", pandoc, [
|
|
4216
|
+
"-t",
|
|
4217
|
+
"plain",
|
|
4218
|
+
document,
|
|
4219
|
+
]);
|
|
4220
|
+
if (!text.stdout.includes("TIANGONG_DOCX_CANARY")) {
|
|
4221
|
+
throw new Error("DOCX text extraction did not return the exact canary sentinel.");
|
|
4222
|
+
}
|
|
4223
|
+
await authoringStep(input, environment, "docx.render", python, [soffice, "--headless", "--convert-to", "pdf", "--outdir", input.temporary, document], 60_000);
|
|
4224
|
+
const pdf = join(input.temporary, "docx-canary.pdf");
|
|
4225
|
+
await requireAuthoringArtifact(pdf, 128);
|
|
4226
|
+
const renderPrefix = join(input.temporary, "docx-page");
|
|
4227
|
+
await authoringStep(input, environment, "docx.rasterize", pdftoppm, [
|
|
4228
|
+
"-f",
|
|
4229
|
+
"1",
|
|
4230
|
+
"-singlefile",
|
|
4231
|
+
"-png",
|
|
4232
|
+
pdf,
|
|
4233
|
+
renderPrefix,
|
|
4234
|
+
]);
|
|
4235
|
+
await requireAuthoringArtifact(`${renderPrefix}.png`, 64);
|
|
4236
|
+
}
|
|
4237
|
+
async function runPdfAuthoringCanary(input, environment) {
|
|
4238
|
+
const python = requireAuthoringRuntimeExecutable(input.runtime?.python ?? null, "Python is missing.");
|
|
4239
|
+
const pdftoppm = requireAuthoringRuntimeExecutable(input.runtime?.pdftoppm ?? null, "Poppler is missing.");
|
|
4240
|
+
const skillDirectory = await verifiedCompanionSkillDirectory(input.plan, input.skill);
|
|
4241
|
+
const converter = join(skillDirectory, "scripts", "convert_pdf_to_images.py");
|
|
4242
|
+
const validationImage = join(skillDirectory, "scripts", "create_validation_image.py");
|
|
4243
|
+
await Promise.all([
|
|
4244
|
+
requireRegularCompanionFile(input.root, converter, "PDF image converter"),
|
|
4245
|
+
requireRegularCompanionFile(input.root, validationImage, "PDF validation image helper"),
|
|
4246
|
+
]);
|
|
4247
|
+
const pdf = join(input.temporary, "pdf-canary.pdf");
|
|
4248
|
+
await authoringStep(input, environment, "pdf.create", python, ["-c", PDF_CANARY_SCRIPT, pdf]);
|
|
4249
|
+
await requireAuthoringArtifact(pdf, 128);
|
|
4250
|
+
const parsed = await authoringStep(input, environment, "pdf.parse", python, [
|
|
4251
|
+
"-c",
|
|
4252
|
+
PDF_VERIFY_SCRIPT,
|
|
4253
|
+
pdf,
|
|
4254
|
+
]);
|
|
4255
|
+
if (!parsed.stdout.includes("TIANGONG_PDF_CANARY")) {
|
|
4256
|
+
throw new Error("PDF parser did not return the exact canary sentinel.");
|
|
4257
|
+
}
|
|
4258
|
+
const renderPrefix = join(input.temporary, "pdf-page");
|
|
4259
|
+
await authoringStep(input, environment, "pdf.rasterize", pdftoppm, [
|
|
4260
|
+
"-f",
|
|
4261
|
+
"1",
|
|
4262
|
+
"-singlefile",
|
|
4263
|
+
"-png",
|
|
4264
|
+
pdf,
|
|
4265
|
+
renderPrefix,
|
|
4266
|
+
]);
|
|
4267
|
+
await requireAuthoringArtifact(`${renderPrefix}.png`, 64);
|
|
4268
|
+
const pages = join(input.temporary, "pdf-pages");
|
|
4269
|
+
await mkdir(pages);
|
|
4270
|
+
await authoringStep(input, environment, "pdf.convert-images", python, [converter, pdf, pages]);
|
|
4271
|
+
const page = join(pages, "page_1.png");
|
|
4272
|
+
await requireAuthoringArtifact(page, 64);
|
|
4273
|
+
const fields = join(input.temporary, "pdf-fields.json");
|
|
4274
|
+
await writeJsonAtomic(fields, { form_fields: [] });
|
|
4275
|
+
const annotated = join(input.temporary, "pdf-validation.png");
|
|
4276
|
+
await authoringStep(input, environment, "pdf.validation-image", python, [
|
|
4277
|
+
validationImage,
|
|
4278
|
+
"1",
|
|
4279
|
+
fields,
|
|
4280
|
+
page,
|
|
4281
|
+
annotated,
|
|
4282
|
+
]);
|
|
4283
|
+
await requireAuthoringArtifact(annotated, 64);
|
|
4284
|
+
}
|
|
4285
|
+
async function runPptxAuthoringCanary(input, environment) {
|
|
4286
|
+
const node = requireAuthoringRuntimeExecutable(input.runtime?.node ?? null, "Node is missing.");
|
|
4287
|
+
const python = requireAuthoringRuntimeExecutable(input.runtime?.python ?? null, "Python is missing.");
|
|
4288
|
+
const skillDirectory = await verifiedCompanionSkillDirectory(input.plan, input.skill);
|
|
4289
|
+
const validator = join(skillDirectory, "scripts", "office", "validate.py");
|
|
4290
|
+
const thumbnail = join(skillDirectory, "scripts", "thumbnail.py");
|
|
4291
|
+
await Promise.all([
|
|
4292
|
+
requireRegularCompanionFile(input.root, validator, "PPTX validator"),
|
|
4293
|
+
requireRegularCompanionFile(input.root, thumbnail, "PPTX thumbnail helper"),
|
|
4294
|
+
]);
|
|
4295
|
+
const presentation = join(input.temporary, "pptx-canary.pptx");
|
|
4296
|
+
await authoringStep(input, environment, "pptx.create", node, [
|
|
4297
|
+
"-e",
|
|
4298
|
+
PPTX_CANARY_SCRIPT,
|
|
4299
|
+
presentation,
|
|
4300
|
+
]);
|
|
4301
|
+
await requireAuthoringArtifact(presentation, 128);
|
|
4302
|
+
await authoringStep(input, environment, "pptx.validate", python, [validator, presentation]);
|
|
4303
|
+
const text = await authoringStep(input, environment, "pptx.extract", python, [
|
|
4304
|
+
"-m",
|
|
4305
|
+
"markitdown",
|
|
4306
|
+
presentation,
|
|
4307
|
+
]);
|
|
4308
|
+
if (!text.stdout.includes("TIANGONG_PPTX_CANARY")) {
|
|
4309
|
+
throw new Error("PPTX MarkItDown did not return the exact canary sentinel.");
|
|
4310
|
+
}
|
|
4311
|
+
const thumbnailPrefix = join(input.temporary, "pptx-grid");
|
|
4312
|
+
await authoringStep(input, environment, "pptx.thumbnail", python, [thumbnail, presentation, thumbnailPrefix, "--cols", "1"], 60_000);
|
|
4313
|
+
await requireAuthoringArtifact(`${thumbnailPrefix}.jpg`, 64);
|
|
4314
|
+
}
|
|
4315
|
+
async function runXlsxAuthoringCanary(input, environment) {
|
|
4316
|
+
const python = requireAuthoringRuntimeExecutable(input.runtime?.python ?? null, "Python is missing.");
|
|
4317
|
+
const skillDirectory = await verifiedCompanionSkillDirectory(input.plan, input.skill);
|
|
4318
|
+
const recalc = join(skillDirectory, "scripts", "recalc.py");
|
|
4319
|
+
await requireRegularCompanionFile(input.root, recalc, "XLSX recalculation helper");
|
|
4320
|
+
const workbook = join(input.temporary, "xlsx-canary.xlsx");
|
|
4321
|
+
await authoringStep(input, environment, "xlsx.create", python, [
|
|
4322
|
+
"-c",
|
|
4323
|
+
XLSX_CANARY_SCRIPT,
|
|
4324
|
+
workbook,
|
|
4325
|
+
]);
|
|
4326
|
+
await requireAuthoringArtifact(workbook, 128);
|
|
4327
|
+
const recalculated = await authoringStep(input, environment, "xlsx.recalculate", python, [recalc, workbook, "60"], 75_000);
|
|
4328
|
+
let result;
|
|
4329
|
+
try {
|
|
4330
|
+
result = JSON.parse(recalculated.stdout);
|
|
4331
|
+
}
|
|
4332
|
+
catch {
|
|
4333
|
+
throw new Error("XLSX recalculation helper did not return JSON.");
|
|
4334
|
+
}
|
|
4335
|
+
if (!isObject(result) || result.status !== "success" || result.total_errors !== 0) {
|
|
4336
|
+
throw new Error("XLSX recalculation helper did not complete without formula errors.");
|
|
4337
|
+
}
|
|
4338
|
+
const cached = await authoringStep(input, environment, "xlsx.verify-cache", python, [
|
|
4339
|
+
"-c",
|
|
4340
|
+
XLSX_VERIFY_SCRIPT,
|
|
4341
|
+
workbook,
|
|
4342
|
+
]);
|
|
4343
|
+
if (cached.stdout.trim() !== "5") {
|
|
4344
|
+
throw new Error("XLSX recalculation did not persist the exact cached formula value.");
|
|
4345
|
+
}
|
|
4346
|
+
const markdown = await authoringStep(input, environment, "xlsx.extract", python, [
|
|
4347
|
+
"-m",
|
|
4348
|
+
"markitdown",
|
|
4349
|
+
workbook,
|
|
4350
|
+
]);
|
|
4351
|
+
const normalizedMarkdown = markdown.stdout.replaceAll("\\_", "_");
|
|
4352
|
+
if (!normalizedMarkdown.includes("TIANGONG_XLSX_CANARY") ||
|
|
4353
|
+
!/(?:^|\D)5(?:\D|$)/u.test(normalizedMarkdown)) {
|
|
4354
|
+
throw new Error("XLSX MarkItDown did not return the exact canary sentinel and value.");
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
async function authoringStep(input, environment, step, command, args, timeoutMs = 30_000) {
|
|
4358
|
+
const result = await input.runner({
|
|
4359
|
+
command,
|
|
4360
|
+
args,
|
|
4361
|
+
cwd: input.root,
|
|
4362
|
+
environment,
|
|
4363
|
+
timeoutMs,
|
|
4364
|
+
});
|
|
4365
|
+
if (result.exitCode !== 0) {
|
|
4366
|
+
throw new Error(`Authoring canary step failed (${step}).`);
|
|
4367
|
+
}
|
|
4368
|
+
return result;
|
|
4369
|
+
}
|
|
4370
|
+
async function requireAuthoringArtifact(path, minimumBytes) {
|
|
4371
|
+
const info = await lstat(path).catch(() => undefined);
|
|
4372
|
+
if (!info?.isFile() || info.isSymbolicLink() || info.size < minimumBytes) {
|
|
4373
|
+
throw new Error("Authoring canary did not create its exact expected regular artifact.");
|
|
4374
|
+
}
|
|
4375
|
+
}
|
|
4376
|
+
const DOCX_CANARY_SCRIPT = String.raw `
|
|
4377
|
+
const fs=require("node:fs"); const {Document,Packer,Paragraph}=require("docx");
|
|
4378
|
+
(async()=>{const out=process.argv[1]; const doc=new Document({sections:[{children:[new Paragraph("TIANGONG_DOCX_CANARY")]}]}); fs.writeFileSync(out,await Packer.toBuffer(doc));})().catch(()=>process.exit(1));`;
|
|
4379
|
+
const PDF_CANARY_SCRIPT = String.raw `
|
|
4380
|
+
import sys
|
|
4381
|
+
from reportlab.pdfgen import canvas
|
|
4382
|
+
c=canvas.Canvas(sys.argv[1]); c.drawString(72,720,"TIANGONG_PDF_CANARY"); c.save()`;
|
|
4383
|
+
const PDF_VERIFY_SCRIPT = String.raw `
|
|
4384
|
+
import sys, pdfplumber
|
|
4385
|
+
from pypdf import PdfReader
|
|
4386
|
+
assert len(PdfReader(sys.argv[1]).pages)==1
|
|
4387
|
+
with pdfplumber.open(sys.argv[1]) as p: print(p.pages[0].extract_text())`;
|
|
4388
|
+
const PPTX_CANARY_SCRIPT = String.raw `
|
|
4389
|
+
const pptxgen=require("pptxgenjs");
|
|
4390
|
+
(async()=>{const p=new pptxgen(); p.addSlide().addText("TIANGONG_PPTX_CANARY",{x:1,y:1,w:6,h:1}); await p.writeFile({fileName:process.argv[1]});})().catch(()=>process.exit(1));`;
|
|
4391
|
+
const XLSX_CANARY_SCRIPT = String.raw `
|
|
4392
|
+
import sys
|
|
4393
|
+
from openpyxl import Workbook
|
|
4394
|
+
w=Workbook(); s=w.active; s["A1"]="TIANGONG_XLSX_CANARY"; s["A2"]=2; s["A3"]=3; s["A4"]="=SUM(A2:A3)"; w.save(sys.argv[1])`;
|
|
4395
|
+
const XLSX_VERIFY_SCRIPT = String.raw `
|
|
4396
|
+
import sys
|
|
4397
|
+
from openpyxl import load_workbook
|
|
4398
|
+
w=load_workbook(sys.argv[1],data_only=True,read_only=True); print(w.active["A4"].value); w.close()`;
|
|
3949
4399
|
async function appendCompanionLiveChecks(checks, input) {
|
|
3950
4400
|
if (input.selected.some((skill) => skill.id === "tiangong.academic-paper-download")) {
|
|
3951
4401
|
await appendSemanticScholarLiveCheck(checks, input);
|